Separate MediaWiki unit and integration tests
authorMáté Szabó <mszabo@wikia-inc.com>
Sat, 1 Jun 2019 14:10:15 +0000 (16:10 +0200)
committerMáté Szabó <mszabo@wikia-inc.com>
Thu, 13 Jun 2019 20:56:31 +0000 (22:56 +0200)
This changeset implements T89432 and related tickets and is based on exploration
done at the Prague Hackathon. The goal is to identify tests in MediaWiki core
that can be run without having to install & configure MediaWiki and its dependencies,
and provide a way to execute these tests via the standard phpunit entry point,
allowing for faster development and integration with existing tooling like IDEs.

The initial set of tests that met these criteria were identified using the work Amir did in
I88822667693d9e00ac3d4639c87bc24e5083e5e8. These tests were then moved into a new subdirectory
under phpunit/ and organized into a separate test suite. The environment for this suite
is set up via a PHPUnit bootstrap file without a custom entry point.

You can execute these tests by running:
$ vendor/bin/phpunit -d memory_limit=512M -c tests/phpunit/unit-tests.xml

Bug: T89432
Bug: T87781
Bug: T84948
Change-Id: Iad01033a0548afd4d2a6f2c1ef6fcc9debf72c0d

426 files changed:
.phpcs.xml
tests/common/TestSetup.php
tests/common/TestsAutoLoader.php
tests/phpunit/MediaWikiUnitTestCase.php [new file with mode: 0644]
tests/phpunit/documentation/ReleaseNotesTest.php [deleted file]
tests/phpunit/includes/CommentStoreCommentTest.php [deleted file]
tests/phpunit/includes/DerivativeRequestTest.php [deleted file]
tests/phpunit/includes/FauxRequestTest.php [deleted file]
tests/phpunit/includes/FauxResponseTest.php [deleted file]
tests/phpunit/includes/FormOptionsInitializationTest.php [deleted file]
tests/phpunit/includes/FormOptionsTest.php [deleted file]
tests/phpunit/includes/GlobalFunctions/wfAppendQueryTest.php [deleted file]
tests/phpunit/includes/GlobalFunctions/wfArrayPlus2dTest.php [deleted file]
tests/phpunit/includes/GlobalFunctions/wfAssembleUrlTest.php [deleted file]
tests/phpunit/includes/GlobalFunctions/wfBaseNameTest.php [deleted file]
tests/phpunit/includes/GlobalFunctions/wfEscapeShellArgTest.php [deleted file]
tests/phpunit/includes/GlobalFunctions/wfGetCallerTest.php [deleted file]
tests/phpunit/includes/GlobalFunctions/wfRemoveDotSegmentsTest.php [deleted file]
tests/phpunit/includes/GlobalFunctions/wfShellExecTest.php [deleted file]
tests/phpunit/includes/GlobalFunctions/wfShorthandToIntegerTest.php [deleted file]
tests/phpunit/includes/GlobalFunctions/wfStringToBoolTest.php [deleted file]
tests/phpunit/includes/GlobalFunctions/wfTimestampTest.php [deleted file]
tests/phpunit/includes/GlobalFunctions/wfUrlencodeTest.php [deleted file]
tests/phpunit/includes/HooksTest.php [deleted file]
tests/phpunit/includes/LicensesTest.php [deleted file]
tests/phpunit/includes/ListToggleTest.php [deleted file]
tests/phpunit/includes/MagicWordFactoryTest.php [deleted file]
tests/phpunit/includes/MediaWikiServicesTest.php [deleted file]
tests/phpunit/includes/MediaWikiVersionFetcherTest.php [deleted file]
tests/phpunit/includes/PathRouterTest.php [deleted file]
tests/phpunit/includes/Revision/FallbackSlotRoleHandlerTest.php [deleted file]
tests/phpunit/includes/Revision/MainSlotRoleHandlerTest.php [deleted file]
tests/phpunit/includes/Revision/RevisionStoreFactoryTest.php [deleted file]
tests/phpunit/includes/Revision/SlotRecordTest.php [deleted file]
tests/phpunit/includes/Revision/SlotRoleHandlerTest.php [deleted file]
tests/phpunit/includes/SanitizerValidateEmailTest.php [deleted file]
tests/phpunit/includes/ServiceWiringTest.php [deleted file]
tests/phpunit/includes/SiteConfigurationTest.php [deleted file]
tests/phpunit/includes/Storage/BlobStoreFactoryTest.php [deleted file]
tests/phpunit/includes/Storage/PreparedEditTest.php [deleted file]
tests/phpunit/includes/TitleArrayFromResultTest.php [deleted file]
tests/phpunit/includes/WikiReferenceTest.php [deleted file]
tests/phpunit/includes/XmlJsTest.php [deleted file]
tests/phpunit/includes/XmlSelectTest.php [deleted file]
tests/phpunit/includes/actions/ViewActionTest.php [deleted file]
tests/phpunit/includes/api/ApiBlockInfoTraitTest.php [deleted file]
tests/phpunit/includes/api/ApiContinuationManagerTest.php [deleted file]
tests/phpunit/includes/api/ApiMessageTest.php [deleted file]
tests/phpunit/includes/api/ApiResultTest.php [deleted file]
tests/phpunit/includes/api/ApiUsageExceptionTest.php [deleted file]
tests/phpunit/includes/auth/AbstractPreAuthenticationProviderTest.php [deleted file]
tests/phpunit/includes/auth/AbstractSecondaryAuthenticationProviderTest.php [deleted file]
tests/phpunit/includes/auth/AuthenticationResponseTest.php [deleted file]
tests/phpunit/includes/auth/ConfirmLinkSecondaryAuthenticationProviderTest.php [deleted file]
tests/phpunit/includes/auth/EmailNotificationSecondaryAuthenticationProviderTest.php [deleted file]
tests/phpunit/includes/changes/ChangesListFilterGroupTest.php [deleted file]
tests/phpunit/includes/collation/CustomUppercaseCollationTest.php [deleted file]
tests/phpunit/includes/composer/ComposerVersionNormalizerTest.php [deleted file]
tests/phpunit/includes/config/ConfigFactoryTest.php [deleted file]
tests/phpunit/includes/config/EtcdConfigTest.php [deleted file]
tests/phpunit/includes/config/HashConfigTest.php [deleted file]
tests/phpunit/includes/config/MultiConfigTest.php [deleted file]
tests/phpunit/includes/config/ServiceOptionsTest.php [deleted file]
tests/phpunit/includes/content/JsonContentHandlerTest.php [deleted file]
tests/phpunit/includes/db/DatabaseOracleTest.php [deleted file]
tests/phpunit/includes/debug/MWDebugTest.php [deleted file]
tests/phpunit/includes/debug/logger/MonologSpiTest.php [deleted file]
tests/phpunit/includes/debug/logger/monolog/AvroFormatterTest.php [deleted file]
tests/phpunit/includes/debug/logger/monolog/CeeFormatterTest.php [deleted file]
tests/phpunit/includes/debug/logger/monolog/KafkaHandlerTest.php [deleted file]
tests/phpunit/includes/debug/logger/monolog/LineFormatterTest.php [deleted file]
tests/phpunit/includes/debug/logger/monolog/LogstashFormatterTest.php [deleted file]
tests/phpunit/includes/deferred/MWCallableUpdateTest.php [deleted file]
tests/phpunit/includes/deferred/TransactionRoundDefiningUpdateTest.php [deleted file]
tests/phpunit/includes/diff/ArrayDiffFormatterTest.php [deleted file]
tests/phpunit/includes/diff/DiffOpTest.php [deleted file]
tests/phpunit/includes/diff/DiffTest.php [deleted file]
tests/phpunit/includes/diff/DifferenceEngineSlotDiffRendererTest.php [deleted file]
tests/phpunit/includes/diff/SlotDiffRendererTest.php [deleted file]
tests/phpunit/includes/exception/HttpErrorTest.php [deleted file]
tests/phpunit/includes/exception/MWExceptionHandlerTest.php [deleted file]
tests/phpunit/includes/exception/ReadOnlyErrorTest.php [deleted file]
tests/phpunit/includes/exception/UserNotLoggedInTest.php [deleted file]
tests/phpunit/includes/externalstore/ExternalStoreFactoryTest.php [deleted file]
tests/phpunit/includes/filebackend/SwiftFileBackendTest.php [deleted file]
tests/phpunit/includes/filerepo/FileBackendDBRepoWrapperTest.php [deleted file]
tests/phpunit/includes/filerepo/FileRepoTest.php [deleted file]
tests/phpunit/includes/htmlform/HTMLAutoCompleteSelectFieldTest.php [deleted file]
tests/phpunit/includes/htmlform/HTMLCheckMatrixTest.php [deleted file]
tests/phpunit/includes/htmlform/HTMLFormTest.php [deleted file]
tests/phpunit/includes/htmlform/HTMLRestrictionsFieldTest.php [deleted file]
tests/phpunit/includes/http/GuzzleHttpRequestTest.php [deleted file]
tests/phpunit/includes/http/HttpRequestFactoryTest.php [deleted file]
tests/phpunit/includes/installer/InstallDocFormatterTest.php [deleted file]
tests/phpunit/includes/installer/OracleInstallerTest.php [deleted file]
tests/phpunit/includes/interwiki/InterwikiLookupAdapterTest.php [deleted file]
tests/phpunit/includes/jobqueue/JobQueueMemoryTest.php [deleted file]
tests/phpunit/includes/json/FormatJsonTest.php [deleted file]
tests/phpunit/includes/libs/ArrayUtilsTest.php [deleted file]
tests/phpunit/includes/libs/CookieTest.php [deleted file]
tests/phpunit/includes/libs/DeferredStringifierTest.php [deleted file]
tests/phpunit/includes/libs/DnsSrvDiscovererTest.php [deleted file]
tests/phpunit/includes/libs/EasyDeflateTest.php [deleted file]
tests/phpunit/includes/libs/GenericArrayObjectTest.php [deleted file]
tests/phpunit/includes/libs/HashRingTest.php [deleted file]
tests/phpunit/includes/libs/HtmlArmorTest.php [deleted file]
tests/phpunit/includes/libs/IEUrlExtensionTest.php [deleted file]
tests/phpunit/includes/libs/IPTest.php [deleted file]
tests/phpunit/includes/libs/JavaScriptMinifierTest.php [deleted file]
tests/phpunit/includes/libs/MapCacheLRUTest.php [deleted file]
tests/phpunit/includes/libs/MemoizedCallableTest.php [deleted file]
tests/phpunit/includes/libs/ProcessCacheLRUTest.php [deleted file]
tests/phpunit/includes/libs/SamplingStatsdClientTest.php [deleted file]
tests/phpunit/includes/libs/StaticArrayWriterTest.php [deleted file]
tests/phpunit/includes/libs/StringUtilsTest.php [deleted file]
tests/phpunit/includes/libs/TimingTest.php [deleted file]
tests/phpunit/includes/libs/XhprofDataTest.php [deleted file]
tests/phpunit/includes/libs/XhprofTest.php [deleted file]
tests/phpunit/includes/libs/XmlTypeCheckTest.php [deleted file]
tests/phpunit/includes/libs/composer/ComposerInstalledTest.php [deleted file]
tests/phpunit/includes/libs/composer/ComposerJsonTest.php [deleted file]
tests/phpunit/includes/libs/composer/ComposerLockTest.php [deleted file]
tests/phpunit/includes/libs/http/HttpAcceptNegotiatorTest.php [deleted file]
tests/phpunit/includes/libs/http/HttpAcceptParserTest.php [deleted file]
tests/phpunit/includes/libs/mime/MSCompoundFileReaderTest.php [deleted file]
tests/phpunit/includes/libs/mime/MimeAnalyzerTest.php [deleted file]
tests/phpunit/includes/libs/objectcache/CachedBagOStuffTest.php [deleted file]
tests/phpunit/includes/libs/objectcache/HashBagOStuffTest.php [deleted file]
tests/phpunit/includes/libs/objectcache/ReplicatedBagOStuffTest.php [deleted file]
tests/phpunit/includes/libs/objectcache/WANObjectCacheTest.php [deleted file]
tests/phpunit/includes/libs/rdbms/ChronologyProtectorTest.php [deleted file]
tests/phpunit/includes/libs/rdbms/TransactionProfilerTest.php [deleted file]
tests/phpunit/includes/libs/rdbms/connectionmanager/ConnectionManagerTest.php [deleted file]
tests/phpunit/includes/libs/rdbms/connectionmanager/SessionConsistentConnectionManagerTest.php [deleted file]
tests/phpunit/includes/libs/rdbms/database/DBConnRefTest.php [deleted file]
tests/phpunit/includes/libs/rdbms/database/DatabaseDomainTest.php [deleted file]
tests/phpunit/includes/libs/rdbms/database/DatabaseMssqlTest.php [deleted file]
tests/phpunit/includes/libs/rdbms/database/DatabaseMysqlBaseTest.php [deleted file]
tests/phpunit/includes/libs/rdbms/database/DatabaseSQLTest.php [deleted file]
tests/phpunit/includes/libs/rdbms/database/DatabaseSqliteRdbmsTest.php [deleted file]
tests/phpunit/includes/libs/rdbms/database/DatabaseTest.php [deleted file]
tests/phpunit/includes/libs/services/ServiceContainerTest.php [deleted file]
tests/phpunit/includes/libs/services/TestWiring1.php [deleted file]
tests/phpunit/includes/libs/services/TestWiring2.php [deleted file]
tests/phpunit/includes/libs/stats/PrefixingStatsdDataFactoryProxyTest.php [deleted file]
tests/phpunit/includes/media/GIFMetadataExtractorTest.php [deleted file]
tests/phpunit/includes/media/IPTCTest.php [deleted file]
tests/phpunit/includes/media/JpegMetadataExtractorTest.php [deleted file]
tests/phpunit/includes/media/MediaHandlerTest.php [deleted file]
tests/phpunit/includes/media/SVGMetadataExtractorTest.php [deleted file]
tests/phpunit/includes/media/WebPHandlerTest.php [deleted file]
tests/phpunit/includes/objectcache/MemcachedBagOStuffTest.php [deleted file]
tests/phpunit/includes/objectcache/RESTBagOStuffTest.php [deleted file]
tests/phpunit/includes/objectcache/RedisBagOStuffTest.php [deleted file]
tests/phpunit/includes/page/ArticleTest.php [deleted file]
tests/phpunit/includes/parser/ParserPreloadTest.php [deleted file]
tests/phpunit/includes/parser/PreprocessorTest.php [deleted file]
tests/phpunit/includes/parser/TidyTest.php [deleted file]
tests/phpunit/includes/password/PasswordFactoryTest.php [deleted file]
tests/phpunit/includes/password/PasswordTest.php [deleted file]
tests/phpunit/includes/preferences/FiltersTest.php [deleted file]
tests/phpunit/includes/registration/ExtensionJsonValidatorTest.php [deleted file]
tests/phpunit/includes/registration/ExtensionProcessorTest.php [deleted file]
tests/phpunit/includes/registration/VersionCheckerTest.php [deleted file]
tests/phpunit/includes/resourceloader/DerivativeResourceLoaderContextTest.php [deleted file]
tests/phpunit/includes/resourceloader/MessageBlobStoreTest.php [deleted file]
tests/phpunit/includes/resourceloader/ResourceLoaderClientHtmlTest.php [deleted file]
tests/phpunit/includes/resourceloader/ResourceLoaderContextTest.php [deleted file]
tests/phpunit/includes/search/SearchIndexFieldTest.php [deleted file]
tests/phpunit/includes/search/SearchSuggestionSetTest.php [deleted file]
tests/phpunit/includes/session/MetadataMergeExceptionTest.php [deleted file]
tests/phpunit/includes/session/SessionIdTest.php [deleted file]
tests/phpunit/includes/session/SessionInfoTest.php [deleted file]
tests/phpunit/includes/session/SessionProviderTest.php [deleted file]
tests/phpunit/includes/session/SessionTest.php [deleted file]
tests/phpunit/includes/session/TokenTest.php [deleted file]
tests/phpunit/includes/shell/CommandFactoryTest.php [deleted file]
tests/phpunit/includes/shell/CommandTest.php [deleted file]
tests/phpunit/includes/shell/FirejailCommandTest.php [deleted file]
tests/phpunit/includes/site/CachingSiteStoreTest.php [deleted file]
tests/phpunit/includes/site/HashSiteStoreTest.php [deleted file]
tests/phpunit/includes/site/MediaWikiPageNameNormalizerTest.php [deleted file]
tests/phpunit/includes/site/SiteExporterTest.php [deleted file]
tests/phpunit/includes/site/SiteImporterTest.php [deleted file]
tests/phpunit/includes/site/SiteImporterTest.xml [deleted file]
tests/phpunit/includes/skins/SkinFactoryTest.php [deleted file]
tests/phpunit/includes/skins/SkinTemplateTest.php [deleted file]
tests/phpunit/includes/skins/SkinTest.php [deleted file]
tests/phpunit/includes/sparql/SparqlClientTest.php [deleted file]
tests/phpunit/includes/specials/ImageListPagerTest.php [deleted file]
tests/phpunit/includes/specials/SpecialUploadTest.php [deleted file]
tests/phpunit/includes/specials/UncategorizedCategoriesPageTest.php [deleted file]
tests/phpunit/includes/tidy/RemexDriverTest.php [deleted file]
tests/phpunit/includes/tidy/html5lib-tests.json [deleted file]
tests/phpunit/includes/title/ForeignTitleTest.php [deleted file]
tests/phpunit/includes/title/NaiveForeignTitleFactoryTest.php [deleted file]
tests/phpunit/includes/title/NamespaceAwareForeignTitleFactoryTest.php [deleted file]
tests/phpunit/includes/title/TitleValueTest.php [deleted file]
tests/phpunit/includes/user/UserArrayFromResultTest.php [deleted file]
tests/phpunit/includes/utils/AvroValidatorTest.php [deleted file]
tests/phpunit/includes/utils/BatchRowUpdateTest.php [deleted file]
tests/phpunit/includes/utils/ClassCollectorTest.php [deleted file]
tests/phpunit/includes/utils/FileContentsHasherTest.php [deleted file]
tests/phpunit/includes/utils/MWCryptHashTest.php [deleted file]
tests/phpunit/includes/utils/MWRestrictionsTest.php [deleted file]
tests/phpunit/includes/utils/UIDGeneratorTest.php [deleted file]
tests/phpunit/includes/utils/ZipDirectoryReaderTest.php [deleted file]
tests/phpunit/includes/watcheditem/NoWriteWatchedItemStoreUnitTest.php [deleted file]
tests/phpunit/languages/SpecialPageAliasTest.php [deleted file]
tests/phpunit/structure/ApiPrefixUniquenessTest.php [deleted file]
tests/phpunit/structure/AutoLoaderStructureTest.php [deleted file]
tests/phpunit/structure/ContentHandlerSanityTest.php [deleted file]
tests/phpunit/structure/PasswordPolicyStructureTest.php [deleted file]
tests/phpunit/structure/StructureTest.php
tests/phpunit/suite.xml
tests/phpunit/unit-tests.xml [new file with mode: 0644]
tests/phpunit/unit/documentation/ReleaseNotesTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/CommentStoreCommentTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/DerivativeRequestTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/FauxRequestTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/FauxResponseTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/FormOptionsInitializationTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/FormOptionsTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/GlobalFunctions/wfAppendQueryTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/GlobalFunctions/wfArrayPlus2dTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/GlobalFunctions/wfAssembleUrlTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/GlobalFunctions/wfBaseNameTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/GlobalFunctions/wfEscapeShellArgTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/GlobalFunctions/wfGetCallerTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/GlobalFunctions/wfRemoveDotSegmentsTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/GlobalFunctions/wfShellExecTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/GlobalFunctions/wfShorthandToIntegerTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/GlobalFunctions/wfStringToBoolTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/GlobalFunctions/wfTimestampTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/GlobalFunctions/wfUrlencodeTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/HooksTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/LicensesTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/ListToggleTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/MagicWordFactoryTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/MediaWikiServicesTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/MediaWikiVersionFetcherTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/PathRouterTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/Revision/FallbackSlotRoleHandlerTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/Revision/MainSlotRoleHandlerTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/Revision/RevisionStoreFactoryTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/Revision/SlotRecordTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/Revision/SlotRoleHandlerTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/SanitizerValidateEmailTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/ServiceWiringTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/SiteConfigurationTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/Storage/BlobStoreFactoryTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/Storage/PreparedEditTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/TitleArrayFromResultTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/WikiReferenceTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/XmlJsTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/XmlSelectTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/actions/ViewActionTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/api/ApiBlockInfoTraitTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/api/ApiContinuationManagerTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/api/ApiMessageTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/api/ApiResultTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/api/ApiUsageExceptionTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/auth/AbstractPreAuthenticationProviderTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/auth/AbstractSecondaryAuthenticationProviderTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/auth/AuthenticationResponseTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/auth/ConfirmLinkSecondaryAuthenticationProviderTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/auth/EmailNotificationSecondaryAuthenticationProviderTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/changes/ChangesListFilterGroupTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/collation/CustomUppercaseCollationTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/composer/ComposerVersionNormalizerTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/config/ConfigFactoryTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/config/EtcdConfigTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/config/HashConfigTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/config/MultiConfigTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/config/ServiceOptionsTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/content/JsonContentHandlerTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/db/DatabaseOracleTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/debug/MWDebugTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/debug/logger/MonologSpiTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/debug/logger/monolog/AvroFormatterTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/debug/logger/monolog/CeeFormatterTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/debug/logger/monolog/KafkaHandlerTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/debug/logger/monolog/LineFormatterTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/debug/logger/monolog/LogstashFormatterTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/deferred/MWCallableUpdateTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/deferred/TransactionRoundDefiningUpdateTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/diff/ArrayDiffFormatterTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/diff/DiffOpTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/diff/DiffTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/diff/DifferenceEngineSlotDiffRendererTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/diff/SlotDiffRendererTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/exception/HttpErrorTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/exception/MWExceptionHandlerTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/exception/ReadOnlyErrorTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/exception/UserNotLoggedInTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/externalstore/ExternalStoreFactoryTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/filebackend/SwiftFileBackendTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/filerepo/FileBackendDBRepoWrapperTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/filerepo/FileRepoTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/htmlform/HTMLAutoCompleteSelectFieldTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/htmlform/HTMLCheckMatrixTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/htmlform/HTMLFormTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/htmlform/HTMLRestrictionsFieldTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/http/GuzzleHttpRequestTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/http/HttpRequestFactoryTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/installer/InstallDocFormatterTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/installer/OracleInstallerTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/interwiki/InterwikiLookupAdapterTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/jobqueue/JobQueueMemoryTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/json/FormatJsonTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/libs/ArrayUtilsTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/libs/CookieTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/libs/DeferredStringifierTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/libs/DnsSrvDiscovererTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/libs/EasyDeflateTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/libs/GenericArrayObjectTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/libs/HashRingTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/libs/HtmlArmorTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/libs/IEUrlExtensionTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/libs/IPTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/libs/JavaScriptMinifierTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/libs/MapCacheLRUTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/libs/MemoizedCallableTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/libs/ProcessCacheLRUTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/libs/SamplingStatsdClientTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/libs/StaticArrayWriterTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/libs/StringUtilsTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/libs/TimingTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/libs/XhprofDataTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/libs/XhprofTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/libs/XmlTypeCheckTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/libs/composer/ComposerInstalledTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/libs/composer/ComposerJsonTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/libs/composer/ComposerLockTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/libs/http/HttpAcceptNegotiatorTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/libs/http/HttpAcceptParserTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/libs/mime/MSCompoundFileReaderTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/libs/mime/MimeAnalyzerTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/libs/objectcache/CachedBagOStuffTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/libs/objectcache/HashBagOStuffTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/libs/objectcache/ReplicatedBagOStuffTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/libs/objectcache/WANObjectCacheTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/libs/rdbms/ChronologyProtectorTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/libs/rdbms/TransactionProfilerTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/libs/rdbms/connectionmanager/ConnectionManagerTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/libs/rdbms/connectionmanager/SessionConsistentConnectionManagerTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/libs/rdbms/database/DBConnRefTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/libs/rdbms/database/DatabaseDomainTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/libs/rdbms/database/DatabaseMssqlTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/libs/rdbms/database/DatabaseMysqlBaseTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/libs/rdbms/database/DatabaseSQLTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/libs/rdbms/database/DatabaseSqliteRdbmsTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/libs/rdbms/database/DatabaseTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/libs/services/ServiceContainerTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/libs/services/TestWiring1.php [new file with mode: 0644]
tests/phpunit/unit/includes/libs/services/TestWiring2.php [new file with mode: 0644]
tests/phpunit/unit/includes/libs/stats/PrefixingStatsdDataFactoryProxyTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/media/GIFMetadataExtractorTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/media/IPTCTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/media/JpegMetadataExtractorTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/media/MediaHandlerTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/media/SVGMetadataExtractorTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/media/WebPHandlerTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/objectcache/MemcachedBagOStuffTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/objectcache/RESTBagOStuffTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/objectcache/RedisBagOStuffTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/page/ArticleTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/parser/ParserPreloadTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/parser/PreprocessorTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/parser/TidyTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/password/PasswordFactoryTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/password/PasswordTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/preferences/FiltersTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/registration/ExtensionJsonValidatorTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/registration/ExtensionProcessorTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/registration/VersionCheckerTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/resourceloader/DerivativeResourceLoaderContextTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/resourceloader/MessageBlobStoreTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/resourceloader/ResourceLoaderClientHtmlTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/resourceloader/ResourceLoaderContextTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/search/SearchIndexFieldTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/search/SearchSuggestionSetTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/session/MetadataMergeExceptionTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/session/SessionIdTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/session/SessionInfoTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/session/SessionProviderTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/session/SessionTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/session/TokenTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/shell/CommandFactoryTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/shell/CommandTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/shell/FirejailCommandTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/site/CachingSiteStoreTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/site/HashSiteStoreTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/site/MediaWikiPageNameNormalizerTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/site/SiteExporterTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/site/SiteImporterTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/site/SiteImporterTest.xml [new file with mode: 0644]
tests/phpunit/unit/includes/skins/SkinFactoryTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/skins/SkinTemplateTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/skins/SkinTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/sparql/SparqlClientTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/specials/ImageListPagerTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/specials/SpecialUploadTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/specials/UncategorizedCategoriesPageTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/tidy/RemexDriverTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/tidy/html5lib-tests.json [new file with mode: 0644]
tests/phpunit/unit/includes/title/ForeignTitleTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/title/NaiveForeignTitleFactoryTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/title/NamespaceAwareForeignTitleFactoryTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/title/TitleValueTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/user/UserArrayFromResultTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/utils/AvroValidatorTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/utils/BatchRowUpdateTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/utils/ClassCollectorTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/utils/FileContentsHasherTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/utils/MWCryptHashTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/utils/MWRestrictionsTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/utils/UIDGeneratorTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/utils/ZipDirectoryReaderTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/watcheditem/NoWriteWatchedItemStoreUnitTest.php [new file with mode: 0644]
tests/phpunit/unit/initUnitTests.php [new file with mode: 0644]
tests/phpunit/unit/languages/SpecialPageAliasTest.php [new file with mode: 0644]
tests/phpunit/unit/structure/ApiPrefixUniquenessTest.php [new file with mode: 0644]
tests/phpunit/unit/structure/AutoLoaderStructureTest.php [new file with mode: 0644]
tests/phpunit/unit/structure/ContentHandlerSanityTest.php [new file with mode: 0644]
tests/phpunit/unit/structure/PasswordPolicyStructureTest.php [new file with mode: 0644]

index 9ccf565..1d5ce0b 100644 (file)
                <exclude-pattern>*/maintenance/storage/trackBlobs\.php</exclude-pattern>
                <!-- Skip violations in some tests for now -->
                <exclude-pattern>*/tests/phpunit/includes/GlobalFunctions/*\.php</exclude-pattern>
+               <exclude-pattern>*/tests/phpunit/unit/includes/GlobalFunctions/*\.php</exclude-pattern>
                <exclude-pattern>*/tests/phpunit/maintenance/*\.php</exclude-pattern>
        </rule>
 
index e24c4c5..6d250be 100644 (file)
@@ -18,6 +18,9 @@ class TestSetup {
                global $wgSessionProviders, $wgSessionPbkdf2Iterations;
                global $wgJobTypeConf;
                global $wgAuthManagerConfig;
+               global $wgSecretKey;
+
+               $wgSecretKey = 'secretsecretsecretsecretsecretsecretsecretsecretsecretsecretsecretsecretsecret';
 
                // wfWarn should cause tests to fail
                $wgDevelopmentWarnings = true;
index 861111a..3eb8c9a 100644 (file)
@@ -60,6 +60,7 @@ $wgAutoloadClasses += [
        'MediaWikiPHPUnitResultPrinter' => "$testDir/phpunit/MediaWikiPHPUnitResultPrinter.php",
        'MediaWikiPHPUnitTestListener' => "$testDir/phpunit/MediaWikiPHPUnitTestListener.php",
        'MediaWikiTestCase' => "$testDir/phpunit/MediaWikiTestCase.php",
+       'MediaWikiUnitTestCase' => "$testDir/phpunit/MediaWikiUnitTestCase.php",
        'MediaWikiTestResult' => "$testDir/phpunit/MediaWikiTestResult.php",
        'MediaWikiTestRunner' => "$testDir/phpunit/MediaWikiTestRunner.php",
        'PHPUnit4And6Compat' => "$testDir/phpunit/PHPUnit4And6Compat.php",
@@ -177,7 +178,7 @@ $wgAutoloadClasses += [
        'LanguageClassesTestCase' => "$testDir/phpunit/languages/LanguageClassesTestCase.php",
 
        # tests/phpunit/includes/libs
-       'GenericArrayObjectTest' => "$testDir/phpunit/includes/libs/GenericArrayObjectTest.php",
+       'GenericArrayObjectTest' => "$testDir/phpunit/unit/includes/libs/GenericArrayObjectTest.php",
 
        # tests/phpunit/maintenance
        'MediaWiki\Tests\Maintenance\DumpAsserter' => "$testDir/phpunit/maintenance/DumpAsserter.php",
diff --git a/tests/phpunit/MediaWikiUnitTestCase.php b/tests/phpunit/MediaWikiUnitTestCase.php
new file mode 100644 (file)
index 0000000..9ecc043
--- /dev/null
@@ -0,0 +1,36 @@
+<?php
+
+use MediaWiki\MediaWikiServices;
+use PHPUnit\Framework\TestCase;
+
+abstract class MediaWikiUnitTestCase extends TestCase {
+       use MediaWikiCoversValidator;
+       use PHPUnit4And6Compat;
+
+       /** @var MediaWikiServices $mwServicesBackup */
+       private $mwServicesBackup;
+
+       /**
+        * Replace global MediaWiki service locator with a clone that has the given overrides applied
+        * @param callable[] $overrides map of service names to instantiators
+        * @throws MWException
+        */
+       protected function overrideMwServices( array $overrides ) {
+               $services = clone MediaWikiServices::getInstance();
+
+               foreach ( $overrides as $serviceName => $factory ) {
+                       $services->disableService( $serviceName );
+                       $services->redefineService( $serviceName, $factory );
+               }
+
+               $this->mwServicesBackup = MediaWikiServices::forceGlobalInstance( $services );
+       }
+
+       protected function tearDown() {
+               parent::tearDown();
+
+               if ( $this->mwServicesBackup ) {
+                       MediaWikiServices::forceGlobalInstance( $this->mwServicesBackup );
+               }
+       }
+}
diff --git a/tests/phpunit/documentation/ReleaseNotesTest.php b/tests/phpunit/documentation/ReleaseNotesTest.php
deleted file mode 100644 (file)
index d20fcff..0000000
+++ /dev/null
@@ -1,69 +0,0 @@
-<?php
-
-/**
- * James doesn't like having to manually fix these things.
- */
-class ReleaseNotesTest extends MediaWikiTestCase {
-       /**
-        * Verify that at least one Release Notes file exists, have content, and
-        * aren't overly long.
-        *
-        * @group documentation
-        * @coversNothing
-        */
-       public function testReleaseNotesFilesExistAndAreNotMalformed() {
-               global $wgVersion, $IP;
-
-               $notesFiles = glob( "$IP/RELEASE-NOTES-*" );
-
-               $this->assertGreaterThanOrEqual(
-                       1,
-                       count( $notesFiles ),
-                       'Repo has at least one Release Notes file.'
-               );
-
-               $versionParts = explode( '.', explode( '-', $wgVersion )[0] );
-               $this->assertContains(
-                       "$IP/RELEASE-NOTES-$versionParts[0].$versionParts[1]",
-                       $notesFiles,
-                       'Repo has a Release Notes file for the current $wgVersion.'
-               );
-
-               foreach ( $notesFiles as $index => $fileName ) {
-                       $this->assertFileLength( "Release Notes", $fileName );
-               }
-
-               // Also test the README and similar files
-               $otherFiles = [
-                       "$IP/COPYING",
-                       "$IP/FAQ",
-                       "$IP/HISTORY",
-                       "$IP/INSTALL",
-                       "$IP/README",
-                       "$IP/SECURITY"
-               ];
-
-               foreach ( $otherFiles as $index => $fileName ) {
-                       $this->assertFileLength( "Help", $fileName );
-               }
-       }
-
-       private function assertFileLength( $type, $fileName ) {
-               $file = file( $fileName, FILE_IGNORE_NEW_LINES );
-
-               $this->assertFalse(
-                       !$file,
-                       "$type file '$fileName' is inaccessible."
-               );
-
-               foreach ( $file as $i => $line ) {
-                       $num = $i + 1;
-                       $this->assertLessThanOrEqual(
-                               // FILE_IGNORE_NEW_LINES drops the \n at the EOL, so max length is 80 not 81.
-                               80,
-                               mb_strlen( $line ),
-                               "$type file '$fileName' line $num, is longer than 80 chars:\n\t'$line'"
-                       );
-               }
-       }
-}
diff --git a/tests/phpunit/includes/CommentStoreCommentTest.php b/tests/phpunit/includes/CommentStoreCommentTest.php
deleted file mode 100644 (file)
index 2dfe03a..0000000
+++ /dev/null
@@ -1,26 +0,0 @@
-<?php
-
-use PHPUnit\Framework\TestCase;
-
-/**
- * @covers CommentStoreComment
- *
- * @license GPL-2.0-or-later
- */
-class CommentStoreCommentTest extends TestCase {
-
-       public function testConstructorWithMessage() {
-               $message = new Message( 'test' );
-               $comment = new CommentStoreComment( null, 'test', $message );
-
-               $this->assertSame( $message, $comment->message );
-       }
-
-       public function testConstructorWithoutMessage() {
-               $text = '{{template|param}}';
-               $comment = new CommentStoreComment( null, $text );
-
-               $this->assertSame( $text, $comment->message->text() );
-       }
-
-}
diff --git a/tests/phpunit/includes/DerivativeRequestTest.php b/tests/phpunit/includes/DerivativeRequestTest.php
deleted file mode 100644 (file)
index f33022b..0000000
+++ /dev/null
@@ -1,21 +0,0 @@
-<?php
-
-/**
- * @covers DerivativeRequest
- */
-class DerivativeRequestTest extends PHPUnit\Framework\TestCase {
-
-       public function testSetIp() {
-               $original = new WebRequest();
-               $original->setIP( '1.2.3.4' );
-               $derivative = new DerivativeRequest( $original, [] );
-
-               $this->assertEquals( '1.2.3.4', $derivative->getIP() );
-
-               $derivative->setIP( '5.6.7.8' );
-
-               $this->assertEquals( '5.6.7.8', $derivative->getIP() );
-               $this->assertEquals( '1.2.3.4', $original->getIP() );
-       }
-
-}
diff --git a/tests/phpunit/includes/FauxRequestTest.php b/tests/phpunit/includes/FauxRequestTest.php
deleted file mode 100644 (file)
index c054caa..0000000
+++ /dev/null
@@ -1,294 +0,0 @@
-<?php
-
-use MediaWiki\Session\SessionManager;
-
-class FauxRequestTest extends PHPUnit\Framework\TestCase {
-
-       use MediaWikiCoversValidator;
-       use PHPUnit4And6Compat;
-
-       public function setUp() {
-               parent::setUp();
-               $this->orgWgServer = $GLOBALS['wgServer'];
-       }
-
-       public function tearDown() {
-               $GLOBALS['wgServer'] = $this->orgWgServer;
-               parent::tearDown();
-       }
-
-       /**
-        * @covers FauxRequest::__construct
-        */
-       public function testConstructInvalidData() {
-               $this->setExpectedException( MWException::class, 'bogus data' );
-               $req = new FauxRequest( 'x' );
-       }
-
-       /**
-        * @covers FauxRequest::__construct
-        */
-       public function testConstructInvalidSession() {
-               $this->setExpectedException( MWException::class, 'bogus session' );
-               $req = new FauxRequest( [], false, 'x' );
-       }
-
-       /**
-        * @covers FauxRequest::__construct
-        */
-       public function testConstructWithSession() {
-               $session = SessionManager::singleton()->getEmptySession( new FauxRequest( [] ) );
-               $this->assertInstanceOf(
-                       FauxRequest::class,
-                       new FauxRequest( [], false, $session )
-               );
-       }
-
-       /**
-        * @covers FauxRequest::getText
-        */
-       public function testGetText() {
-               $req = new FauxRequest( [ 'x' => 'Value' ] );
-               $this->assertEquals( 'Value', $req->getText( 'x' ) );
-               $this->assertEquals( '', $req->getText( 'z' ) );
-       }
-
-       /**
-        * Integration test for parent method
-        * @covers FauxRequest::getVal
-        */
-       public function testGetVal() {
-               $req = new FauxRequest( [ 'crlf' => "A\r\nb" ] );
-               $this->assertSame( "A\r\nb", $req->getVal( 'crlf' ), 'CRLF' );
-       }
-
-       /**
-        * Integration test for parent method
-        * @covers FauxRequest::getRawVal
-        */
-       public function testGetRawVal() {
-               $req = new FauxRequest( [
-                       'x' => 'Value',
-                       'y' => [ 'a' ],
-                       'crlf' => "A\r\nb"
-               ] );
-               $this->assertSame( 'Value', $req->getRawVal( 'x' ) );
-               $this->assertSame( null, $req->getRawVal( 'z' ), 'Not found' );
-               $this->assertSame( null, $req->getRawVal( 'y' ), 'Array is ignored' );
-               $this->assertSame( "A\r\nb", $req->getRawVal( 'crlf' ), 'CRLF' );
-       }
-
-       /**
-        * @covers FauxRequest::getValues
-        */
-       public function testGetValues() {
-               $values = [ 'x' => 'Value', 'y' => '' ];
-               $req = new FauxRequest( $values );
-               $this->assertEquals( $values, $req->getValues() );
-       }
-
-       /**
-        * @covers FauxRequest::getQueryValues
-        */
-       public function testGetQueryValues() {
-               $values = [ 'x' => 'Value', 'y' => '' ];
-
-               $req = new FauxRequest( $values );
-               $this->assertEquals( $values, $req->getQueryValues() );
-               $req = new FauxRequest( $values, /*wasPosted*/ true );
-               $this->assertEquals( [], $req->getQueryValues() );
-       }
-
-       /**
-        * @covers FauxRequest::getMethod
-        */
-       public function testGetMethod() {
-               $req = new FauxRequest( [] );
-               $this->assertEquals( 'GET', $req->getMethod() );
-               $req = new FauxRequest( [], /*wasPosted*/ true );
-               $this->assertEquals( 'POST', $req->getMethod() );
-       }
-
-       /**
-        * @covers FauxRequest::wasPosted
-        */
-       public function testWasPosted() {
-               $req = new FauxRequest( [] );
-               $this->assertFalse( $req->wasPosted() );
-               $req = new FauxRequest( [], /*wasPosted*/ true );
-               $this->assertTrue( $req->wasPosted() );
-       }
-
-       /**
-        * @covers FauxRequest::getCookie
-        * @covers FauxRequest::setCookie
-        * @covers FauxRequest::setCookies
-        */
-       public function testCookies() {
-               $req = new FauxRequest();
-               $this->assertSame( null, $req->getCookie( 'z', '' ) );
-
-               $req->setCookie( 'x', 'Value', '' );
-               $this->assertEquals( 'Value', $req->getCookie( 'x', '' ) );
-
-               $req->setCookies( [ 'x' => 'One', 'y' => 'Two' ], '' );
-               $this->assertEquals( 'One', $req->getCookie( 'x', '' ) );
-               $this->assertEquals( 'Two', $req->getCookie( 'y', '' ) );
-       }
-
-       /**
-        * @covers FauxRequest::getCookie
-        * @covers FauxRequest::setCookie
-        * @covers FauxRequest::setCookies
-        */
-       public function testCookiesDefaultPrefix() {
-               global $wgCookiePrefix;
-               $oldPrefix = $wgCookiePrefix;
-               $wgCookiePrefix = '_';
-
-               $req = new FauxRequest();
-               $this->assertSame( null, $req->getCookie( 'z' ) );
-
-               $req->setCookie( 'x', 'Value' );
-               $this->assertEquals( 'Value', $req->getCookie( 'x' ) );
-
-               $wgCookiePrefix = $oldPrefix;
-       }
-
-       /**
-        * @covers FauxRequest::getRequestURL
-        */
-       public function testGetRequestURL_disallowed() {
-               $req = new FauxRequest();
-               $this->setExpectedException( MWException::class );
-               $req->getRequestURL();
-       }
-
-       /**
-        * @covers FauxRequest::setRequestURL
-        * @covers FauxRequest::getRequestURL
-        */
-       public function testSetRequestURL() {
-               $req = new FauxRequest();
-               $req->setRequestURL( 'https://example.org' );
-               $this->assertEquals( 'https://example.org', $req->getRequestURL() );
-       }
-
-       /**
-        * @covers FauxRequest::getFullRequestURL
-        */
-       public function testGetFullRequestURL_disallowed() {
-               $GLOBALS['wgServer'] = '//wiki.test';
-               $req = new FauxRequest();
-
-               $this->setExpectedException( MWException::class );
-               $req->getFullRequestURL();
-       }
-
-       /**
-        * @covers FauxRequest::getFullRequestURL
-        */
-       public function testGetFullRequestURL_http() {
-               $GLOBALS['wgServer'] = '//wiki.test';
-               $req = new FauxRequest();
-               $req->setRequestURL( '/path' );
-
-               $this->assertSame(
-                       'http://wiki.test/path',
-                       $req->getFullRequestURL()
-               );
-       }
-
-       /**
-        * @covers FauxRequest::getFullRequestURL
-        */
-       public function testGetFullRequestURL_https() {
-               $GLOBALS['wgServer'] = '//wiki.test';
-               $req = new FauxRequest( [], false, null, 'https' );
-               $req->setRequestURL( '/path' );
-
-               $this->assertSame(
-                       'https://wiki.test/path',
-                       $req->getFullRequestURL()
-               );
-       }
-
-       /**
-        * @covers FauxRequest::__construct
-        * @covers FauxRequest::getProtocol
-        */
-       public function testProtocol() {
-               $req = new FauxRequest();
-               $this->assertEquals( 'http', $req->getProtocol() );
-               $req = new FauxRequest( [], false, null, 'http' );
-               $this->assertEquals( 'http', $req->getProtocol() );
-               $req = new FauxRequest( [], false, null, 'https' );
-               $this->assertEquals( 'https', $req->getProtocol() );
-       }
-
-       /**
-        * @covers FauxRequest::setHeader
-        * @covers FauxRequest::setHeaders
-        * @covers FauxRequest::getHeader
-        */
-       public function testGetSetHeader() {
-               $value = 'text/plain, text/html';
-
-               $request = new FauxRequest();
-               $request->setHeader( 'Accept', $value );
-
-               $this->assertEquals( $request->getHeader( 'Nonexistent' ), false );
-               $this->assertEquals( $request->getHeader( 'Accept' ), $value );
-               $this->assertEquals( $request->getHeader( 'ACCEPT' ), $value );
-               $this->assertEquals( $request->getHeader( 'accept' ), $value );
-               $this->assertEquals(
-                       $request->getHeader( 'Accept', WebRequest::GETHEADER_LIST ),
-                       [ 'text/plain', 'text/html' ]
-               );
-       }
-
-       /**
-        * @covers FauxRequest::initHeaders
-        */
-       public function testGetAllHeaders() {
-               $_SERVER['HTTP_TEST'] = 'Example';
-
-               $request = new FauxRequest();
-
-               $this->assertEquals(
-                       [],
-                       $request->getAllHeaders()
-               );
-
-               $this->assertEquals(
-                       false,
-                       $request->getHeader( 'test' )
-               );
-       }
-
-       /**
-        * @covers FauxRequest::__construct
-        * @covers FauxRequest::getSessionArray
-        */
-       public function testSessionData() {
-               $values = [ 'x' => 'Value', 'y' => '' ];
-
-               $req = new FauxRequest( [], false, /*session*/ $values );
-               $this->assertEquals( $values, $req->getSessionArray() );
-
-               $req = new FauxRequest();
-               $this->assertSame( null, $req->getSessionArray() );
-       }
-
-       /**
-        * @covers FauxRequest::getRawQueryString
-        * @covers FauxRequest::getRawPostString
-        * @covers FauxRequest::getRawInput
-        */
-       public function testDummies() {
-               $req = new FauxRequest();
-               $this->assertEquals( '', $req->getRawQueryString() );
-               $this->assertEquals( '', $req->getRawPostString() );
-               $this->assertEquals( '', $req->getRawInput() );
-       }
-}
diff --git a/tests/phpunit/includes/FauxResponseTest.php b/tests/phpunit/includes/FauxResponseTest.php
deleted file mode 100644 (file)
index 8085bc7..0000000
+++ /dev/null
@@ -1,146 +0,0 @@
-<?php
-/**
- * Copyright @ 2011 Alexandre Emsenhuber
- *
- * 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; either version 2 of the License, or
- * (at your option) any later version.
- *
- * 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.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- */
-
-class FauxResponseTest extends MediaWikiTestCase {
-       /** @var FauxResponse */
-       protected $response;
-
-       protected function setUp() {
-               parent::setUp();
-               $this->response = new FauxResponse;
-       }
-
-       /**
-        * @covers FauxResponse::setCookie
-        * @covers FauxResponse::getCookie
-        * @covers FauxResponse::getCookieData
-        * @covers FauxResponse::getCookies
-        */
-       public function testCookie() {
-               $expire = time() + 100;
-               $cookie = [
-                       'value' => 'val',
-                       'path' => '/path',
-                       'domain' => 'domain',
-                       'secure' => true,
-                       'httpOnly' => false,
-                       'raw' => false,
-                       'expire' => $expire,
-               ];
-
-               $this->assertEquals( null, $this->response->getCookie( 'xkey' ), 'Non-existing cookie' );
-               $this->response->setCookie( 'key', 'val', $expire, [
-                       'prefix' => 'x',
-                       'path' => '/path',
-                       'domain' => 'domain',
-                       'secure' => 1,
-                       'httpOnly' => 0,
-               ] );
-               $this->assertEquals( 'val', $this->response->getCookie( 'xkey' ), 'Existing cookie' );
-               $this->assertEquals( $cookie, $this->response->getCookieData( 'xkey' ),
-                       'Existing cookie (data)' );
-               $this->assertEquals( [ 'xkey' => $cookie ], $this->response->getCookies(),
-                       'Existing cookies' );
-       }
-
-       /**
-        * @covers FauxResponse::getheader
-        * @covers FauxResponse::header
-        */
-       public function testHeader() {
-               $this->assertEquals( null, $this->response->getHeader( 'Location' ), 'Non-existing header' );
-
-               $this->response->header( 'Location: http://localhost/' );
-               $this->assertEquals(
-                       'http://localhost/',
-                       $this->response->getHeader( 'Location' ),
-                       'Set header'
-               );
-
-               $this->response->header( 'Location: http://127.0.0.1/' );
-               $this->assertEquals(
-                       'http://127.0.0.1/',
-                       $this->response->getHeader( 'Location' ),
-                       'Same header'
-               );
-
-               $this->response->header( 'Location: http://127.0.0.2/', false );
-               $this->assertEquals(
-                       'http://127.0.0.1/',
-                       $this->response->getHeader( 'Location' ),
-                       'Same header with override disabled'
-               );
-
-               $this->response->header( 'Location: http://localhost/' );
-               $this->assertEquals(
-                       'http://localhost/',
-                       $this->response->getHeader( 'LOCATION' ),
-                       'Get header case insensitive'
-               );
-       }
-
-       /**
-        * @covers FauxResponse::getStatusCode
-        */
-       public function testResponseCode() {
-               $this->response->header( 'HTTP/1.1 200' );
-               $this->assertEquals( 200, $this->response->getStatusCode(), 'Header with no message' );
-
-               $this->response->header( 'HTTP/1.x 201' );
-               $this->assertEquals(
-                       201,
-                       $this->response->getStatusCode(),
-                       'Header with no message and protocol 1.x'
-               );
-
-               $this->response->header( 'HTTP/1.1 202 OK' );
-               $this->assertEquals( 202, $this->response->getStatusCode(), 'Normal header' );
-
-               $this->response->header( 'HTTP/1.x 203 OK' );
-               $this->assertEquals(
-                       203,
-                       $this->response->getStatusCode(),
-                       'Normal header with no message and protocol 1.x'
-               );
-
-               $this->response->header( 'HTTP/1.x 204 OK', false, 205 );
-               $this->assertEquals(
-                       205,
-                       $this->response->getStatusCode(),
-                       'Third parameter overrides the HTTP/... header'
-               );
-
-               $this->response->statusHeader( 210 );
-               $this->assertEquals(
-                       210,
-                       $this->response->getStatusCode(),
-                       'Handle statusHeader method'
-               );
-
-               $this->response->header( 'Location: http://localhost/', false, 206 );
-               $this->assertEquals(
-                       206,
-                       $this->response->getStatusCode(),
-                       'Third parameter with another header'
-               );
-       }
-}
diff --git a/tests/phpunit/includes/FormOptionsInitializationTest.php b/tests/phpunit/includes/FormOptionsInitializationTest.php
deleted file mode 100644 (file)
index 2c78618..0000000
+++ /dev/null
@@ -1,70 +0,0 @@
-<?php
-
-use Wikimedia\TestingAccessWrapper;
-
-/**
- * Test class for FormOptions initialization
- * Ensure the FormOptions::add() does what we want it to do.
- *
- * Copyright © 2011, Antoine Musso
- *
- * @author Antoine Musso
- */
-class FormOptionsInitializationTest extends MediaWikiTestCase {
-       /**
-        * @var FormOptions
-        */
-       protected $object;
-
-       /**
-        * A new fresh and empty FormOptions object to test initialization
-        * with.
-        */
-       protected function setUp() {
-               parent::setUp();
-               $this->object = TestingAccessWrapper::newFromObject( new FormOptions() );
-       }
-
-       /**
-        * @covers FormOptions::add
-        */
-       public function testAddStringOption() {
-               $this->object->add( 'foo', 'string value' );
-               $this->assertEquals(
-                       [
-                               'foo' => [
-                                       'default' => 'string value',
-                                       'consumed' => false,
-                                       'type' => FormOptions::STRING,
-                                       'value' => null,
-                               ]
-                       ],
-                       $this->object->options
-               );
-       }
-
-       /**
-        * @covers FormOptions::add
-        */
-       public function testAddIntegers() {
-               $this->object->add( 'one', 1 );
-               $this->object->add( 'negone', -1 );
-               $this->assertEquals(
-                       [
-                               'negone' => [
-                                       'default' => -1,
-                                       'value' => null,
-                                       'consumed' => false,
-                                       'type' => FormOptions::INT,
-                               ],
-                               'one' => [
-                                       'default' => 1,
-                                       'value' => null,
-                                       'consumed' => false,
-                                       'type' => FormOptions::INT,
-                               ]
-                       ],
-                       $this->object->options
-               );
-       }
-}
diff --git a/tests/phpunit/includes/FormOptionsTest.php b/tests/phpunit/includes/FormOptionsTest.php
deleted file mode 100644 (file)
index da08670..0000000
+++ /dev/null
@@ -1,105 +0,0 @@
-<?php
-/**
- * This file host two test case classes for the MediaWiki FormOptions class:
- *  - FormOptionsInitializationTest : tests initialization of the class.
- *  - FormOptionsTest : tests methods an on instance
- *
- * The split let us take advantage of setting up a fixture for the methods
- * tests.
- */
-
-/**
- * Test class for FormOptions methods.
- *
- * Copyright © 2011, Antoine Musso
- *
- * @author Antoine Musso
- */
-class FormOptionsTest extends MediaWikiTestCase {
-       /**
-        * @var FormOptions
-        */
-       protected $object;
-
-       /**
-        * Instanciates a FormOptions object to play with.
-        * FormOptions::add() is tested by the class FormOptionsInitializationTest
-        * so we assume the function is well tested already an use it to create
-        * the fixture.
-        */
-       protected function setUp() {
-               parent::setUp();
-               $this->object = new FormOptions;
-               $this->object->add( 'string1', 'string one' );
-               $this->object->add( 'string2', 'string two' );
-               $this->object->add( 'integer', 0 );
-               $this->object->add( 'float', 0.0 );
-               $this->object->add( 'intnull', 0, FormOptions::INTNULL );
-       }
-
-       /** Helpers for testGuessType() */
-       /* @{ */
-       private function assertGuessBoolean( $data ) {
-               $this->guess( FormOptions::BOOL, $data );
-       }
-
-       private function assertGuessInt( $data ) {
-               $this->guess( FormOptions::INT, $data );
-       }
-
-       private function assertGuessFloat( $data ) {
-               $this->guess( FormOptions::FLOAT, $data );
-       }
-
-       private function assertGuessString( $data ) {
-               $this->guess( FormOptions::STRING, $data );
-       }
-
-       private function assertGuessArray( $data ) {
-               $this->guess( FormOptions::ARR, $data );
-       }
-
-       /** Generic helper */
-       private function guess( $expected, $data ) {
-               $this->assertEquals(
-                       $expected,
-                       FormOptions::guessType( $data )
-               );
-       }
-
-       /* @} */
-
-       /**
-        * Reuse helpers above assertGuessBoolean assertGuessInt assertGuessString
-        * @covers FormOptions::guessType
-        */
-       public function testGuessTypeDetection() {
-               $this->assertGuessBoolean( true );
-               $this->assertGuessBoolean( false );
-
-               $this->assertGuessInt( 0 );
-               $this->assertGuessInt( -5 );
-               $this->assertGuessInt( 5 );
-               $this->assertGuessInt( 0x0F );
-
-               $this->assertGuessFloat( 0.0 );
-               $this->assertGuessFloat( 1.5 );
-               $this->assertGuessFloat( 1e3 );
-
-               $this->assertGuessString( 'true' );
-               $this->assertGuessString( 'false' );
-               $this->assertGuessString( '5' );
-               $this->assertGuessString( '0' );
-               $this->assertGuessString( '1.5' );
-
-               $this->assertGuessArray( [ 'foo' ] );
-       }
-
-       /**
-        * @expectedException MWException
-        * @covers FormOptions::guessType
-        */
-       public function testGuessTypeOnNullThrowException() {
-               $this->object->guessType( null );
-       }
-}
diff --git a/tests/phpunit/includes/GlobalFunctions/wfAppendQueryTest.php b/tests/phpunit/includes/GlobalFunctions/wfAppendQueryTest.php
deleted file mode 100644 (file)
index bb71610..0000000
+++ /dev/null
@@ -1,79 +0,0 @@
-<?php
-
-/**
- * @group GlobalFunctions
- * @covers ::wfAppendQuery
- */
-class WfAppendQueryTest extends MediaWikiTestCase {
-       /**
-        * @dataProvider provideAppendQuery
-        */
-       public function testAppendQuery( $url, $query, $expected, $message = null ) {
-               $this->assertEquals( $expected, wfAppendQuery( $url, $query ), $message );
-       }
-
-       public static function provideAppendQuery() {
-               return [
-                       [
-                               'http://www.example.org/index.php',
-                               '',
-                               'http://www.example.org/index.php',
-                               'No query'
-                       ],
-                       [
-                               'http://www.example.org/index.php',
-                               [ 'foo' => 'bar' ],
-                               'http://www.example.org/index.php?foo=bar',
-                               'Set query array'
-                       ],
-                       [
-                               'http://www.example.org/index.php?foz=baz',
-                               'foo=bar',
-                               'http://www.example.org/index.php?foz=baz&foo=bar',
-                               'Set query string'
-                       ],
-                       [
-                               'http://www.example.org/index.php?foo=bar',
-                               '',
-                               'http://www.example.org/index.php?foo=bar',
-                               'Empty string with query'
-                       ],
-                       [
-                               'http://www.example.org/index.php?foo=bar',
-                               [ 'baz' => 'quux' ],
-                               'http://www.example.org/index.php?foo=bar&baz=quux',
-                               'Add query array'
-                       ],
-                       [
-                               'http://www.example.org/index.php?foo=bar',
-                               'baz=quux',
-                               'http://www.example.org/index.php?foo=bar&baz=quux',
-                               'Add query string'
-                       ],
-                       [
-                               'http://www.example.org/index.php?foo=bar',
-                               [ 'baz' => 'quux', 'foo' => 'baz' ],
-                               'http://www.example.org/index.php?foo=bar&baz=quux&foo=baz',
-                               'Modify query array'
-                       ],
-                       [
-                               'http://www.example.org/index.php?foo=bar',
-                               'baz=quux&foo=baz',
-                               'http://www.example.org/index.php?foo=bar&baz=quux&foo=baz',
-                               'Modify query string'
-                       ],
-                       [
-                               'http://www.example.org/index.php#baz',
-                               'foo=bar',
-                               'http://www.example.org/index.php?foo=bar#baz',
-                               'URL with fragment'
-                       ],
-                       [
-                               'http://www.example.org/index.php?foo=bar#baz',
-                               'quux=blah',
-                               'http://www.example.org/index.php?foo=bar&quux=blah#baz',
-                               'URL with query string and fragment'
-                       ]
-               ];
-       }
-}
diff --git a/tests/phpunit/includes/GlobalFunctions/wfArrayPlus2dTest.php b/tests/phpunit/includes/GlobalFunctions/wfArrayPlus2dTest.php
deleted file mode 100644 (file)
index 65b56ef..0000000
+++ /dev/null
@@ -1,94 +0,0 @@
-<?php
-/**
- * @group GlobalFunctions
- * @covers ::wfArrayPlus2d
- */
-class WfArrayPlus2dTest extends MediaWikiTestCase {
-       /**
-        * @dataProvider provideArrays
-        */
-       public function testWfArrayPlus2d( $baseArray, $newValues, $expected, $testName ) {
-               $this->assertEquals(
-                       $expected,
-                       wfArrayPlus2d( $baseArray, $newValues ),
-                       $testName
-               );
-       }
-
-       /**
-        * Provider for testing wfArrayPlus2d
-        *
-        * @return array
-        */
-       public static function provideArrays() {
-               return [
-                       // target array, new values array, expected result
-                       [
-                               [ 0 => '1dArray' ],
-                               [ 1 => '1dArray' ],
-                               [ 0 => '1dArray', 1 => '1dArray' ],
-                               "Test simple union of two arrays with different keys",
-                       ],
-                       [
-                               [
-                                       0 => [ 0 => '2dArray' ],
-                               ],
-                               [
-                                       0 => [ 1 => '2dArray' ],
-                               ],
-                               [
-                                       0 => [ 0 => '2dArray', 1 => '2dArray' ],
-                               ],
-                               "Test union of 2d arrays with different keys in the value array",
-                       ],
-                       [
-                               [
-                                       0 => [ 0 => '2dArray' ],
-                               ],
-                               [
-                                       0 => [ 0 => '1dArray' ],
-                               ],
-                               [
-                                       0 => [ 0 => '2dArray' ],
-                               ],
-                               "Test union of 2d arrays with same keys in the value array",
-                       ],
-                       [
-                               [
-                                       0 => [ 0 => [ 0 => '3dArray' ] ],
-                               ],
-                               [
-                                       0 => [ 0 => [ 1 => '2dArray' ] ],
-                               ],
-                               [
-                                       0 => [ 0 => [ 0 => '3dArray' ] ],
-                               ],
-                               "Test union of 3d array with different keys",
-                       ],
-                       [
-                               [
-                                       0 => [ 0 => [ 0 => '3dArray' ] ],
-                               ],
-                               [
-                                       0 => [ 1 => [ 0 => '2dArray' ] ],
-                               ],
-                               [
-                                       0 => [ 0 => [ 0 => '3dArray' ], 1 => [ 0 => '2dArray' ] ],
-                               ],
-                               "Test union of 3d array with different keys in the value array",
-                       ],
-                       [
-                               [
-                                       0 => [ 0 => [ 0 => '3dArray' ] ],
-                               ],
-                               [
-                                       0 => [ 0 => [ 0 => '2dArray' ] ],
-                               ],
-                               [
-                                       0 => [ 0 => [ 0 => '3dArray' ] ],
-                               ],
-                               "Test union of 3d array with same keys in the value array",
-                       ],
-               ];
-       }
-}
diff --git a/tests/phpunit/includes/GlobalFunctions/wfAssembleUrlTest.php b/tests/phpunit/includes/GlobalFunctions/wfAssembleUrlTest.php
deleted file mode 100644 (file)
index 7ddad36..0000000
+++ /dev/null
@@ -1,112 +0,0 @@
-<?php
-/**
- * @group GlobalFunctions
- * @covers ::wfAssembleUrl
- */
-class WfAssembleUrlTest extends MediaWikiTestCase {
-       /**
-        * @dataProvider provideURLParts
-        */
-       public function testWfAssembleUrl( $parts, $output ) {
-               $partsDump = print_r( $parts, true );
-               $this->assertEquals(
-                       $output,
-                       wfAssembleUrl( $parts ),
-                       "Testing $partsDump assembles to $output"
-               );
-       }
-
-       /**
-        * Provider of URL parts for testing wfAssembleUrl()
-        *
-        * @return array
-        */
-       public static function provideURLParts() {
-               $schemes = [
-                       '' => [],
-                       '//' => [
-                               'delimiter' => '//',
-                       ],
-                       'http://' => [
-                               'scheme' => 'http',
-                               'delimiter' => '://',
-                       ],
-               ];
-
-               $hosts = [
-                       '' => [],
-                       'example.com' => [
-                               'host' => 'example.com',
-                       ],
-                       'example.com:123' => [
-                               'host' => 'example.com',
-                               'port' => 123,
-                       ],
-                       'id@example.com' => [
-                               'user' => 'id',
-                               'host' => 'example.com',
-                       ],
-                       'id@example.com:123' => [
-                               'user' => 'id',
-                               'host' => 'example.com',
-                               'port' => 123,
-                       ],
-                       'id:key@example.com' => [
-                               'user' => 'id',
-                               'pass' => 'key',
-                               'host' => 'example.com',
-                       ],
-                       'id:key@example.com:123' => [
-                               'user' => 'id',
-                               'pass' => 'key',
-                               'host' => 'example.com',
-                               'port' => 123,
-                       ],
-               ];
-
-               $cases = [];
-               foreach ( $schemes as $scheme => $schemeParts ) {
-                       foreach ( $hosts as $host => $hostParts ) {
-                               foreach ( [ '', '/path' ] as $path ) {
-                                       foreach ( [ '', 'query' ] as $query ) {
-                                               foreach ( [ '', 'fragment' ] as $fragment ) {
-                                                       $parts = array_merge(
-                                                               $schemeParts,
-                                                               $hostParts
-                                                       );
-                                                       $url = $scheme .
-                                                               $host .
-                                                               $path;
-
-                                                       if ( $path ) {
-                                                               $parts['path'] = $path;
-                                                       }
-                                                       if ( $query ) {
-                                                               $parts['query'] = $query;
-                                                               $url .= '?' . $query;
-                                                       }
-                                                       if ( $fragment ) {
-                                                               $parts['fragment'] = $fragment;
-                                                               $url .= '#' . $fragment;
-                                                       }
-
-                                                       $cases[] = [
-                                                               $parts,
-                                                               $url,
-                                                       ];
-                                               }
-                                       }
-                               }
-                       }
-               }
-
-               $complexURL = 'http://id:key@example.org:321' .
-                       '/over/there?name=ferret&foo=bar#nose';
-               $cases[] = [
-                       wfParseUrl( $complexURL ),
-                       $complexURL,
-               ];
-
-               return $cases;
-       }
-}
diff --git a/tests/phpunit/includes/GlobalFunctions/wfBaseNameTest.php b/tests/phpunit/includes/GlobalFunctions/wfBaseNameTest.php
deleted file mode 100644 (file)
index 78e09e6..0000000
+++ /dev/null
@@ -1,40 +0,0 @@
-<?php
-/**
- * @group GlobalFunctions
- * @covers ::wfBaseName
- */
-class WfBaseNameTest extends MediaWikiTestCase {
-       /**
-        * @dataProvider providePaths
-        */
-       public function testBaseName( $fullpath, $basename ) {
-               $this->assertEquals( $basename, wfBaseName( $fullpath ),
-                       "wfBaseName('$fullpath') => '$basename'" );
-       }
-
-       public static function providePaths() {
-               return [
-                       [ '', '' ],
-                       [ '/', '' ],
-                       [ '\\', '' ],
-                       [ '//', '' ],
-                       [ '\\\\', '' ],
-                       [ 'a', 'a' ],
-                       [ 'aaaa', 'aaaa' ],
-                       [ '/a', 'a' ],
-                       [ '\\a', 'a' ],
-                       [ '/aaaa', 'aaaa' ],
-                       [ '\\aaaa', 'aaaa' ],
-                       [ '/aaaa/', 'aaaa' ],
-                       [ '\\aaaa\\', 'aaaa' ],
-                       [ '\\aaaa\\', 'aaaa' ],
-                       [
-                               '/mnt/upload3/wikipedia/en/thumb/8/8b/'
-                                       . 'Zork_Grand_Inquisitor_box_cover.jpg/93px-Zork_Grand_Inquisitor_box_cover.jpg',
-                               '93px-Zork_Grand_Inquisitor_box_cover.jpg'
-                       ],
-                       [ 'C:\\Progra~1\\Wikime~1\\Wikipe~1\\VIEWER.EXE', 'VIEWER.EXE' ],
-                       [ 'Östergötland_coat_of_arms.png', 'Östergötland_coat_of_arms.png' ],
-               ];
-       }
-}
diff --git a/tests/phpunit/includes/GlobalFunctions/wfEscapeShellArgTest.php b/tests/phpunit/includes/GlobalFunctions/wfEscapeShellArgTest.php
deleted file mode 100644 (file)
index 7402054..0000000
+++ /dev/null
@@ -1,43 +0,0 @@
-<?php
-
-/**
- * @group GlobalFunctions
- * @covers ::wfEscapeShellArg
- */
-class WfEscapeShellArgTest extends MediaWikiTestCase {
-       public function testSingleInput() {
-               if ( wfIsWindows() ) {
-                       $expected = '"blah"';
-               } else {
-                       $expected = "'blah'";
-               }
-
-               $actual = wfEscapeShellArg( 'blah' );
-
-               $this->assertEquals( $expected, $actual );
-       }
-
-       public function testMultipleArgs() {
-               if ( wfIsWindows() ) {
-                       $expected = '"foo" "bar" "baz"';
-               } else {
-                       $expected = "'foo' 'bar' 'baz'";
-               }
-
-               $actual = wfEscapeShellArg( 'foo', 'bar', 'baz' );
-
-               $this->assertEquals( $expected, $actual );
-       }
-
-       public function testMultipleArgsAsArray() {
-               if ( wfIsWindows() ) {
-                       $expected = '"foo" "bar" "baz"';
-               } else {
-                       $expected = "'foo' 'bar' 'baz'";
-               }
-
-               $actual = wfEscapeShellArg( [ 'foo', 'bar', 'baz' ] );
-
-               $this->assertEquals( $expected, $actual );
-       }
-}
diff --git a/tests/phpunit/includes/GlobalFunctions/wfGetCallerTest.php b/tests/phpunit/includes/GlobalFunctions/wfGetCallerTest.php
deleted file mode 100644 (file)
index 8a7bfa5..0000000
+++ /dev/null
@@ -1,46 +0,0 @@
-<?php
-
-/**
- * @group GlobalFunctions
- * @covers ::wfGetCaller
- */
-class WfGetCallerTest extends MediaWikiTestCase {
-       public function testZero() {
-               $this->assertEquals( 'WfGetCallerTest->testZero', wfGetCaller( 1 ) );
-       }
-
-       function callerOne() {
-               return wfGetCaller();
-       }
-
-       public function testOne() {
-               $this->assertEquals( 'WfGetCallerTest->testOne', self::callerOne() );
-       }
-
-       static function intermediateFunction( $level = 2, $n = 0 ) {
-               if ( $n > 0 ) {
-                       return self::intermediateFunction( $level, $n - 1 );
-               }
-
-               return wfGetCaller( $level );
-       }
-
-       public function testTwo() {
-               $this->assertEquals( 'WfGetCallerTest->testTwo', self::intermediateFunction() );
-       }
-
-       public function testN() {
-               $this->assertEquals( 'WfGetCallerTest->testN', self::intermediateFunction( 2, 0 ) );
-               $this->assertEquals(
-                       'WfGetCallerTest::intermediateFunction',
-                       self::intermediateFunction( 1, 0 )
-               );
-
-               for ( $i = 0; $i < 10; $i++ ) {
-                       $this->assertEquals(
-                               'WfGetCallerTest::intermediateFunction',
-                               self::intermediateFunction( $i + 1, $i )
-                       );
-               }
-       }
-}
diff --git a/tests/phpunit/includes/GlobalFunctions/wfRemoveDotSegmentsTest.php b/tests/phpunit/includes/GlobalFunctions/wfRemoveDotSegmentsTest.php
deleted file mode 100644 (file)
index eae5588..0000000
+++ /dev/null
@@ -1,93 +0,0 @@
-<?php
-
-/**
- * @group GlobalFunctions
- * @covers ::wfRemoveDotSegments
- */
-class WfRemoveDotSegmentsTest extends MediaWikiTestCase {
-       /**
-        * @dataProvider providePaths
-        */
-       public function testWfRemoveDotSegments( $inputPath, $outputPath ) {
-               $this->assertEquals(
-                       $outputPath,
-                       wfRemoveDotSegments( $inputPath ),
-                       "Testing $inputPath expands to $outputPath"
-               );
-       }
-
-       /**
-        * Provider of URL paths for testing wfRemoveDotSegments()
-        *
-        * @return array
-        */
-       public static function providePaths() {
-               return [
-                       [ '/a/b/c/./../../g', '/a/g' ],
-                       [ 'mid/content=5/../6', 'mid/6' ],
-                       [ '/a//../b', '/a/b' ],
-                       [ '/.../a', '/.../a' ],
-                       [ '.../a', '.../a' ],
-                       [ '', '' ],
-                       [ '/', '/' ],
-                       [ '//', '//' ],
-                       [ '.', '' ],
-                       [ '..', '' ],
-                       [ '...', '...' ],
-                       [ '/.', '/' ],
-                       [ '/..', '/' ],
-                       [ './', '' ],
-                       [ '../', '' ],
-                       [ './a', 'a' ],
-                       [ '../a', 'a' ],
-                       [ '../../a', 'a' ],
-                       [ '.././a', 'a' ],
-                       [ './../a', 'a' ],
-                       [ '././a', 'a' ],
-                       [ '../../', '' ],
-                       [ '.././', '' ],
-                       [ './../', '' ],
-                       [ '././', '' ],
-                       [ '../..', '' ],
-                       [ '../.', '' ],
-                       [ './..', '' ],
-                       [ './.', '' ],
-                       [ '/../../a', '/a' ],
-                       [ '/.././a', '/a' ],
-                       [ '/./../a', '/a' ],
-                       [ '/././a', '/a' ],
-                       [ '/../../', '/' ],
-                       [ '/.././', '/' ],
-                       [ '/./../', '/' ],
-                       [ '/././', '/' ],
-                       [ '/../..', '/' ],
-                       [ '/../.', '/' ],
-                       [ '/./..', '/' ],
-                       [ '/./.', '/' ],
-                       [ 'b/../../a', '/a' ],
-                       [ 'b/.././a', '/a' ],
-                       [ 'b/./../a', '/a' ],
-                       [ 'b/././a', 'b/a' ],
-                       [ 'b/../../', '/' ],
-                       [ 'b/.././', '/' ],
-                       [ 'b/./../', '/' ],
-                       [ 'b/././', 'b/' ],
-                       [ 'b/../..', '/' ],
-                       [ 'b/../.', '/' ],
-                       [ 'b/./..', '/' ],
-                       [ 'b/./.', 'b/' ],
-                       [ '/b/../../a', '/a' ],
-                       [ '/b/.././a', '/a' ],
-                       [ '/b/./../a', '/a' ],
-                       [ '/b/././a', '/b/a' ],
-                       [ '/b/../../', '/' ],
-                       [ '/b/.././', '/' ],
-                       [ '/b/./../', '/' ],
-                       [ '/b/././', '/b/' ],
-                       [ '/b/../..', '/' ],
-                       [ '/b/../.', '/' ],
-                       [ '/b/./..', '/' ],
-                       [ '/b/./.', '/b/' ],
-               ];
-       }
-}
diff --git a/tests/phpunit/includes/GlobalFunctions/wfShellExecTest.php b/tests/phpunit/includes/GlobalFunctions/wfShellExecTest.php
deleted file mode 100644 (file)
index 6279cf6..0000000
+++ /dev/null
@@ -1,20 +0,0 @@
-<?php
-
-/**
- * @group GlobalFunctions
- * @covers ::wfShellExec
- */
-class WfShellExecTest extends MediaWikiTestCase {
-       public function testT69870() {
-               $command = wfIsWindows()
-                       // 333 = 331 + CRLF
-                       ? ( 'for /l %i in (1, 1, 1001) do @echo ' . str_repeat( '*', 331 ) )
-                       : 'printf "%-333333s" "*"';
-
-               // Test several times because it involves a race condition that may randomly succeed or fail
-               for ( $i = 0; $i < 10; $i++ ) {
-                       $output = wfShellExec( $command );
-                       $this->assertEquals( 333333, strlen( $output ) );
-               }
-       }
-}
diff --git a/tests/phpunit/includes/GlobalFunctions/wfShorthandToIntegerTest.php b/tests/phpunit/includes/GlobalFunctions/wfShorthandToIntegerTest.php
deleted file mode 100644 (file)
index 40b2e63..0000000
+++ /dev/null
@@ -1,31 +0,0 @@
-<?php
-
-/**
- * @group GlobalFunctions
- * @covers ::wfShorthandToInteger
- */
-class WfShorthandToIntegerTest extends MediaWikiTestCase {
-       /**
-        * @dataProvider provideABunchOfShorthands
-        */
-       public function testWfShorthandToInteger( $input, $output, $description ) {
-               $this->assertEquals(
-                       wfShorthandToInteger( $input ),
-                       $output,
-                       $description
-               );
-       }
-
-       public static function provideABunchOfShorthands() {
-               return [
-                       [ '', -1, 'Empty string' ],
-                       [ '     ', -1, 'String of spaces' ],
-                       [ '1G', 1024 * 1024 * 1024, 'One gig uppercased' ],
-                       [ '1g', 1024 * 1024 * 1024, 'One gig lowercased' ],
-                       [ '1M', 1024 * 1024, 'One meg uppercased' ],
-                       [ '1m', 1024 * 1024, 'One meg lowercased' ],
-                       [ '1K', 1024, 'One kb uppercased' ],
-                       [ '1k', 1024, 'One kb lowercased' ],
-               ];
-       }
-}
diff --git a/tests/phpunit/includes/GlobalFunctions/wfStringToBoolTest.php b/tests/phpunit/includes/GlobalFunctions/wfStringToBoolTest.php
deleted file mode 100644 (file)
index 7f56b60..0000000
+++ /dev/null
@@ -1,51 +0,0 @@
-<?php
-
-/**
- * @group GlobalFunctions
- * @covers ::wfStringToBool
- */
-class WfStringToBoolTest extends MediaWikiTestCase {
-
-       public function getTestCases() {
-               return [
-                       [ 'true', true ],
-                       [ 'on', true ],
-                       [ 'yes', true ],
-                       [ 'TRUE', true ],
-                       [ 'YeS', true ],
-                       [ 'On', true ],
-                       [ '1', true ],
-                       [ '+1', true ],
-                       [ '01', true ],
-                       [ '-001', true ],
-                       [ '  1', true ],
-                       [ '-1  ', true ],
-                       [ '', false ],
-                       [ '0', false ],
-                       [ 'false', false ],
-                       [ 'NO', false ],
-                       [ 'NOT', false ],
-                       [ 'never', false ],
-                       [ '!&', false ],
-                       [ '-0', false ],
-                       [ '+0', false ],
-                       [ 'forget about it', false ],
-                       [ ' on', false ],
-                       [ 'true ', false ],
-               ];
-       }
-
-       /**
-        * @dataProvider getTestCases
-        * @param string $str
-        * @param bool $bool
-        */
-       public function testStr2Bool( $str, $bool ) {
-               if ( $bool ) {
-                       $this->assertTrue( wfStringToBool( $str ) );
-               } else {
-                       $this->assertFalse( wfStringToBool( $str ) );
-               }
-       }
-
-}
diff --git a/tests/phpunit/includes/GlobalFunctions/wfTimestampTest.php b/tests/phpunit/includes/GlobalFunctions/wfTimestampTest.php
deleted file mode 100644 (file)
index a70f136..0000000
+++ /dev/null
@@ -1,194 +0,0 @@
-<?php
-
-/**
- * @group GlobalFunctions
- * @covers ::wfTimestamp
- */
-class WfTimestampTest extends MediaWikiTestCase {
-       /**
-        * @dataProvider provideNormalTimestamps
-        */
-       public function testNormalTimestamps( $input, $format, $output, $desc ) {
-               $this->assertEquals( $output, wfTimestamp( $format, $input ), $desc );
-       }
-
-       public static function provideNormalTimestamps() {
-               $t = gmmktime( 12, 34, 56, 1, 15, 2001 );
-
-               return [
-                       // TS_UNIX
-                       [ $t, TS_MW, '20010115123456', 'TS_UNIX to TS_MW' ],
-                       [ -30281104, TS_MW, '19690115123456', 'Negative TS_UNIX to TS_MW' ],
-                       [ $t, TS_UNIX, 979562096, 'TS_UNIX to TS_UNIX' ],
-                       [ $t, TS_DB, '2001-01-15 12:34:56', 'TS_UNIX to TS_DB' ],
-                       [ $t + 0.01, TS_MW, '20010115123456', 'TS_UNIX float to TS_MW' ],
-
-                       [ $t, TS_ISO_8601_BASIC, '20010115T123456Z', 'TS_ISO_8601_BASIC to TS_DB' ],
-
-                       // TS_MW
-                       [ '20010115123456', TS_MW, '20010115123456', 'TS_MW to TS_MW' ],
-                       [ '20010115123456', TS_UNIX, 979562096, 'TS_MW to TS_UNIX' ],
-                       [ '20010115123456', TS_DB, '2001-01-15 12:34:56', 'TS_MW to TS_DB' ],
-                       [ '20010115123456', TS_ISO_8601_BASIC, '20010115T123456Z', 'TS_MW to TS_ISO_8601_BASIC' ],
-
-                       // TS_DB
-                       [ '2001-01-15 12:34:56', TS_MW, '20010115123456', 'TS_DB to TS_MW' ],
-                       [ '2001-01-15 12:34:56', TS_UNIX, 979562096, 'TS_DB to TS_UNIX' ],
-                       [ '2001-01-15 12:34:56', TS_DB, '2001-01-15 12:34:56', 'TS_DB to TS_DB' ],
-                       [
-                               '2001-01-15 12:34:56',
-                               TS_ISO_8601_BASIC,
-                               '20010115T123456Z',
-                               'TS_DB to TS_ISO_8601_BASIC'
-                       ],
-
-                       # rfc2822 section 3.3
-                       [ '20010115123456', TS_RFC2822, 'Mon, 15 Jan 2001 12:34:56 GMT', 'TS_MW to TS_RFC2822' ],
-                       [ 'Mon, 15 Jan 2001 12:34:56 GMT', TS_MW, '20010115123456', 'TS_RFC2822 to TS_MW' ],
-                       [
-                               ' Mon, 15 Jan 2001 12:34:56 GMT',
-                               TS_MW,
-                               '20010115123456',
-                               'TS_RFC2822 with leading space to TS_MW'
-                       ],
-                       [
-                               '15 Jan 2001 12:34:56 GMT',
-                               TS_MW,
-                               '20010115123456',
-                               'TS_RFC2822 without optional day-of-week to TS_MW'
-                       ],
-
-                       # FWS = ([*WSP CRLF] 1*WSP) / obs-FWS ; Folding white space
-                       # obs-FWS = 1*WSP *(CRLF 1*WSP) ; Section 4.2
-                       [ 'Mon, 15         Jan 2001 12:34:56 GMT', TS_MW, '20010115123456', 'TS_RFC2822 to TS_MW' ],
-
-                       # WSP = SP / HTAB ; rfc2234
-                       [
-                               "Mon, 15 Jan\x092001 12:34:56 GMT",
-                               TS_MW,
-                               '20010115123456',
-                               'TS_RFC2822 with HTAB to TS_MW'
-                       ],
-                       [
-                               "Mon, 15 Jan\x09 \x09  2001 12:34:56 GMT",
-                               TS_MW,
-                               '20010115123456',
-                               'TS_RFC2822 with HTAB and SP to TS_MW'
-                       ],
-                       [
-                               'Sun, 6 Nov 94 08:49:37 GMT',
-                               TS_MW,
-                               '19941106084937',
-                               'TS_RFC2822 with obsolete year to TS_MW'
-                       ],
-               ];
-       }
-
-       /**
-        * This test checks wfTimestamp() with values outside.
-        * It needs PHP 64 bits or PHP > 5.1.
-        * See r74778 and T27451
-        * @dataProvider provideOldTimestamps
-        */
-       public function testOldTimestamps( $input, $outputType, $output, $message ) {
-               $timestamp = wfTimestamp( $outputType, $input );
-               if ( substr( $output, 0, 1 ) === '/' ) {
-                       // T66946: Day of the week calculations for very old
-                       // timestamps varies from system to system.
-                       $this->assertRegExp( $output, $timestamp, $message );
-               } else {
-                       $this->assertEquals( $output, $timestamp, $message );
-               }
-       }
-
-       public static function provideOldTimestamps() {
-               return [
-                       [
-                               '19011213204554',
-                               TS_RFC2822,
-                               'Fri, 13 Dec 1901 20:45:54 GMT',
-                               'Earliest time according to PHP documentation'
-                       ],
-                       [ '20380119031407', TS_RFC2822, 'Tue, 19 Jan 2038 03:14:07 GMT', 'Latest 32 bit time' ],
-                       [ '19011213204552', TS_UNIX, '-2147483648', 'Earliest 32 bit unix time' ],
-                       [ '20380119031407', TS_UNIX, '2147483647', 'Latest 32 bit unix time' ],
-                       [ '19011213204552', TS_RFC2822, 'Fri, 13 Dec 1901 20:45:52 GMT', 'Earliest 32 bit time' ],
-                       [
-                               '19011213204551',
-                               TS_RFC2822,
-                               'Fri, 13 Dec 1901 20:45:51 GMT', 'Earliest 32 bit time - 1'
-                       ],
-                       [ '20380119031408', TS_RFC2822, 'Tue, 19 Jan 2038 03:14:08 GMT', 'Latest 32 bit time + 1' ],
-                       [ '19011212000000', TS_MW, '19011212000000', 'Convert to itself r74778#c10645' ],
-                       [ '19011213204551', TS_UNIX, '-2147483649', 'Earliest 32 bit unix time - 1' ],
-                       [ '20380119031408', TS_UNIX, '2147483648', 'Latest 32 bit unix time + 1' ],
-                       [ '-2147483649', TS_MW, '19011213204551', '1901 negative unix time to MediaWiki' ],
-                       [ '-5331871504', TS_MW, '18010115123456', '1801 negative unix time to MediaWiki' ],
-                       [
-                               '0117-08-09 12:34:56',
-                               TS_RFC2822,
-                               '/, 09 Aug 0117 12:34:56 GMT$/',
-                               'Death of Roman Emperor [[Trajan]]'
-                       ],
-
-                       /* @todo FIXME: 00 to 101 years are taken as being in [1970-2069] */
-                       [ '-58979923200', TS_RFC2822, '/, 01 Jan 0101 00:00:00 GMT$/', '1/1/101' ],
-                       [ '-62135596800', TS_RFC2822, 'Mon, 01 Jan 0001 00:00:00 GMT', 'Year 1' ],
-
-                       /* It is not clear if we should generate a year 0 or not
-                        * We are completely off RFC2822 requirement of year being
-                        * 1900 or later.
-                        */
-                       [
-                               '-62142076800',
-                               TS_RFC2822,
-                               'Wed, 18 Oct 0000 00:00:00 GMT',
-                               'ISO 8601:2004 [[year 0]], also called [[1 BC]]'
-                       ],
-               ];
-       }
-
-       /**
-        * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.3.1
-        * @dataProvider provideHttpDates
-        */
-       public function testHttpDate( $input, $output, $desc ) {
-               $this->assertEquals( $output, wfTimestamp( TS_MW, $input ), $desc );
-       }
-
-       public static function provideHttpDates() {
-               return [
-                       [ 'Sun, 06 Nov 1994 08:49:37 GMT', '19941106084937', 'RFC 822 date' ],
-                       [ 'Sunday, 06-Nov-94 08:49:37 GMT', '19941106084937', 'RFC 850 date' ],
-                       [ 'Sun Nov  6 08:49:37 1994', '19941106084937', "ANSI C's asctime() format" ],
-                       // See http://www.squid-cache.org/mail-archive/squid-users/200307/0122.html and r77171
-                       [
-                               'Mon, 22 Nov 2010 14:12:42 GMT; length=52626',
-                               '20101122141242',
-                               'Netscape extension to HTTP/1.0'
-                       ],
-               ];
-       }
-
-       /**
-        * There are a number of assumptions in our codebase where wfTimestamp()
-        * should give the current date but it is not given a 0 there. See r71751 CR
-        */
-       public function testTimestampParameter() {
-               $now = wfTimestamp( TS_UNIX );
-               // We check that wfTimestamp doesn't return false (error) and use a LessThan assert
-               // for the cases where the test is run in a second boundary.
-
-               $zero = wfTimestamp( TS_UNIX, 0 );
-               $this->assertNotEquals( false, $zero );
-               $this->assertLessThan( 5, $zero - $now );
-
-               $empty = wfTimestamp( TS_UNIX, '' );
-               $this->assertNotEquals( false, $empty );
-               $this->assertLessThan( 5, $empty - $now );
-
-               $null = wfTimestamp( TS_UNIX, null );
-               $this->assertNotEquals( false, $null );
-               $this->assertLessThan( 5, $null - $now );
-       }
-}
diff --git a/tests/phpunit/includes/GlobalFunctions/wfUrlencodeTest.php b/tests/phpunit/includes/GlobalFunctions/wfUrlencodeTest.php
deleted file mode 100644 (file)
index 5d9f63d..0000000
+++ /dev/null
@@ -1,123 +0,0 @@
-<?php
-
-/**
- * The function only need a string parameter and might react to IIS7.0
- *
- * @group GlobalFunctions
- * @covers ::wfUrlencode
- */
-class WfUrlencodeTest extends MediaWikiTestCase {
-       # ### TESTS ##############################################################
-
-       /**
-        * @dataProvider provideURLS
-        */
-       public function testEncodingUrlWith( $input, $expected ) {
-               $this->verifyEncodingFor( 'Apache', $input, $expected );
-       }
-
-       /**
-        * @dataProvider provideURLS
-        */
-       public function testEncodingUrlWithMicrosoftIis7( $input, $expected ) {
-               $this->verifyEncodingFor( 'Microsoft-IIS/7', $input, $expected );
-       }
-
-       # ### HELPERS #############################################################
-
-       /**
-        * Internal helper that actually run the test.
-        * Called by the public methods testEncodingUrlWith...()
-        */
-       private function verifyEncodingFor( $server, $input, $expectations ) {
-               $expected = $this->extractExpect( $server, $expectations );
-
-               // save up global
-               $old = $_SERVER['SERVER_SOFTWARE'] ?? null;
-               $_SERVER['SERVER_SOFTWARE'] = $server;
-               wfUrlencode( null );
-
-               // do the requested test
-               $this->assertEquals(
-                       $expected,
-                       wfUrlencode( $input ),
-                       "Encoding '$input' for server '$server' should be '$expected'"
-               );
-
-               // restore global
-               if ( $old === null ) {
-                       unset( $_SERVER['SERVER_SOFTWARE'] );
-               } else {
-                       $_SERVER['SERVER_SOFTWARE'] = $old;
-               }
-               wfUrlencode( null );
-       }
-
-       /**
-        * Interprets the provider array. Return expected value depending
-        * the HTTP server name.
-        */
-       private function extractExpect( $server, $expectations ) {
-               if ( is_string( $expectations ) ) {
-                       return $expectations;
-               } elseif ( is_array( $expectations ) ) {
-                       if ( !array_key_exists( $server, $expectations ) ) {
-                               throw new MWException( __METHOD__ . " expectation does not have any "
-                                       . "value for server name $server. Check the provider array.\n" );
-                       } else {
-                               return $expectations[$server];
-                       }
-               } else {
-                       throw new MWException( __METHOD__ . " given invalid expectation for "
-                               . "'$server'. Should be a string or an array( <http server name> => <string> ).\n" );
-               }
-       }
-
-       # ### PROVIDERS ###########################################################
-
-       /**
-        * Format is either:
-        *   [ 'input', 'expected' ];
-        * Or:
-        *   [ 'input',
-        *       [ 'Apache', 'expected' ],
-        *       [ 'Microsoft-IIS/7', 'expected' ],
-        *   ],
-        * If you want to add other HTTP server name, you will have to add a new
-        * testing method much like the testEncodingUrlWith() method above.
-        */
-       public static function provideURLS() {
-               return [
-                       # ## RFC 1738 chars
-                       // + is not safe
-                       [ '+', '%2B' ],
-                       // & and = not safe in queries
-                       [ '&', '%26' ],
-                       [ '=', '%3D' ],
-
-                       [ ':', [
-                               'Apache' => ':',
-                               'Microsoft-IIS/7' => '%3A',
-                       ] ],
-
-                       // remaining chars do not need encoding
-                       [
-                               ';@$-_.!*',
-                               ';@$-_.!*',
-                       ],
-
-                       # ## Other tests
-                       // slash remain unchanged. %2F seems to break things
-                       [ '/', '/' ],
-                       // T105265
-                       [ '~', '~' ],
-
-                       // Other 'funnies' chars
-                       [ '[]', '%5B%5D' ],
-                       [ '<>', '%3C%3E' ],
-
-                       // Apostrophe is encoded
-                       [ '\'', '%27' ],
-               ];
-       }
-}
diff --git a/tests/phpunit/includes/HooksTest.php b/tests/phpunit/includes/HooksTest.php
deleted file mode 100644 (file)
index c66b712..0000000
+++ /dev/null
@@ -1,332 +0,0 @@
-<?php
-
-class HooksTest extends MediaWikiTestCase {
-
-       function setUp() {
-               global $wgHooks;
-               parent::setUp();
-               Hooks::clear( 'MediaWikiHooksTest001' );
-               unset( $wgHooks['MediaWikiHooksTest001'] );
-       }
-
-       public static function provideHooks() {
-               $i = new NothingClass();
-
-               return [
-                       [
-                               'Object and method',
-                               [ $i, 'someNonStatic' ],
-                               'changed-nonstatic',
-                               'changed-nonstatic'
-                       ],
-                       [ 'Object and no method', [ $i ], 'changed-onevent', 'original' ],
-                       [
-                               'Object and method with data',
-                               [ $i, 'someNonStaticWithData', 'data' ],
-                               'data',
-                               'original'
-                       ],
-                       [ 'Object and static method', [ $i, 'someStatic' ], 'changed-static', 'original' ],
-                       [
-                               'Class::method static call',
-                               [ 'NothingClass::someStatic' ],
-                               'changed-static',
-                               'original'
-                       ],
-                       [
-                               'Class::method static call as array',
-                               [ [ 'NothingClass::someStatic' ] ],
-                               'changed-static',
-                               'original'
-                       ],
-                       [ 'Global function', [ 'NothingFunction' ], 'changed-func', 'original' ],
-                       [ 'Global function with data', [ 'NothingFunctionData', 'data' ], 'data', 'original' ],
-                       [ 'Closure', [ function ( &$foo, $bar ) {
-                               $foo = 'changed-closure';
-
-                               return true;
-                       } ], 'changed-closure', 'original' ],
-                       [ 'Closure with data', [ function ( $data, &$foo, $bar ) {
-                               $foo = $data;
-
-                               return true;
-                       }, 'data' ], 'data', 'original' ]
-               ];
-       }
-
-       /**
-        * @dataProvider provideHooks
-        * @covers Hooks::register
-        * @covers Hooks::run
-        * @covers Hooks::callHook
-        */
-       public function testNewStyleHooks( $msg, $hook, $expectedFoo, $expectedBar ) {
-               $foo = $bar = 'original';
-
-               Hooks::register( 'MediaWikiHooksTest001', $hook );
-               Hooks::run( 'MediaWikiHooksTest001', [ &$foo, &$bar ] );
-
-               $this->assertSame( $expectedFoo, $foo, $msg );
-               $this->assertSame( $expectedBar, $bar, $msg );
-       }
-
-       /**
-        * @covers Hooks::getHandlers
-        */
-       public function testGetHandlers() {
-               global $wgHooks;
-
-               $this->assertSame(
-                       [],
-                       Hooks::getHandlers( 'MediaWikiHooksTest001' ),
-                       'No hooks registered'
-               );
-
-               $a = new NothingClass();
-               $b = new NothingClass();
-
-               $wgHooks['MediaWikiHooksTest001'][] = $a;
-
-               $this->assertSame(
-                       [ $a ],
-                       Hooks::getHandlers( 'MediaWikiHooksTest001' ),
-                       'Hook registered by $wgHooks'
-               );
-
-               Hooks::register( 'MediaWikiHooksTest001', $b );
-               $this->assertSame(
-                       [ $b, $a ],
-                       Hooks::getHandlers( 'MediaWikiHooksTest001' ),
-                       'Hooks::getHandlers() should return hooks registered via wgHooks as well as Hooks::register'
-               );
-
-               Hooks::clear( 'MediaWikiHooksTest001' );
-               unset( $wgHooks['MediaWikiHooksTest001'] );
-
-               Hooks::register( 'MediaWikiHooksTest001', $b );
-               $this->assertSame(
-                       [ $b ],
-                       Hooks::getHandlers( 'MediaWikiHooksTest001' ),
-                       'Hook registered by Hook::register'
-               );
-       }
-
-       /**
-        * @covers Hooks::isRegistered
-        * @covers Hooks::register
-        * @covers Hooks::run
-        * @covers Hooks::callHook
-        */
-       public function testNewStyleHookInteraction() {
-               global $wgHooks;
-
-               $a = new NothingClass();
-               $b = new NothingClass();
-
-               $wgHooks['MediaWikiHooksTest001'][] = $a;
-               $this->assertTrue(
-                       Hooks::isRegistered( 'MediaWikiHooksTest001' ),
-                       'Hook registered via $wgHooks should be noticed by Hooks::isRegistered'
-               );
-
-               Hooks::register( 'MediaWikiHooksTest001', $b );
-               $this->assertEquals(
-                       2,
-                       count( Hooks::getHandlers( 'MediaWikiHooksTest001' ) ),
-                       'Hooks::getHandlers() should return hooks registered via wgHooks as well as Hooks::register'
-               );
-
-               $foo = 'quux';
-               $bar = 'qaax';
-
-               Hooks::run( 'MediaWikiHooksTest001', [ &$foo, &$bar ] );
-               $this->assertEquals(
-                       1,
-                       $a->calls,
-                       'Hooks::run() should run hooks registered via wgHooks as well as Hooks::register'
-               );
-               $this->assertEquals(
-                       1,
-                       $b->calls,
-                       'Hooks::run() should run hooks registered via wgHooks as well as Hooks::register'
-               );
-       }
-
-       /**
-        * @expectedException MWException
-        * @covers Hooks::run
-        * @covers Hooks::callHook
-        */
-       public function testUncallableFunction() {
-               Hooks::register( 'MediaWikiHooksTest001', 'ThisFunctionDoesntExist' );
-               Hooks::run( 'MediaWikiHooksTest001', [] );
-       }
-
-       /**
-        * @covers Hooks::run
-        * @covers Hooks::callHook
-        */
-       public function testFalseReturn() {
-               Hooks::register( 'MediaWikiHooksTest001', function ( &$foo ) {
-                       return false;
-               } );
-               Hooks::register( 'MediaWikiHooksTest001', function ( &$foo ) {
-                       $foo = 'test';
-
-                       return true;
-               } );
-               $foo = 'original';
-               Hooks::run( 'MediaWikiHooksTest001', [ &$foo ] );
-               $this->assertSame( 'original', $foo, 'Hooks abort after a false return.' );
-       }
-
-       /**
-        * @covers Hooks::run
-        */
-       public function testNullReturn() {
-               Hooks::register( 'MediaWikiHooksTest001', function ( &$foo ) {
-                       return;
-               } );
-               Hooks::register( 'MediaWikiHooksTest001', function ( &$foo ) {
-                       $foo = 'test';
-
-                       return true;
-               } );
-               $foo = 'original';
-               Hooks::run( 'MediaWikiHooksTest001', [ &$foo ] );
-               $this->assertSame( 'test', $foo, 'Hooks continue after a null return.' );
-       }
-
-       /**
-        * @covers Hooks::callHook
-        */
-       public function testCallHook_FalseHook() {
-               Hooks::register( 'MediaWikiHooksTest001', false );
-               Hooks::register( 'MediaWikiHooksTest001', function ( &$foo ) {
-                       $foo = 'test';
-
-                       return true;
-               } );
-               $foo = 'original';
-               Hooks::run( 'MediaWikiHooksTest001', [ &$foo ] );
-               $this->assertSame( 'test', $foo, 'Hooks that are falsey are skipped.' );
-       }
-
-       /**
-        * @covers Hooks::callHook
-        * @expectedException MWException
-        */
-       public function testCallHook_UnknownDatatype() {
-               Hooks::register( 'MediaWikiHooksTest001', 12345 );
-               Hooks::run( 'MediaWikiHooksTest001' );
-       }
-
-       /**
-        * @covers Hooks::callHook
-        * @expectedException PHPUnit_Framework_Error_Deprecated
-        */
-       public function testCallHook_Deprecated() {
-               Hooks::register( 'MediaWikiHooksTest001', 'NothingClass::someStatic' );
-               Hooks::run( 'MediaWikiHooksTest001', [], '1.31' );
-       }
-
-       /**
-        * @covers Hooks::runWithoutAbort
-        * @covers Hooks::callHook
-        */
-       public function testRunWithoutAbort() {
-               $list = [];
-               Hooks::register( 'MediaWikiHooksTest001', function ( &$list ) {
-                       $list[] = 1;
-                       return true; // Explicit true
-               } );
-               Hooks::register( 'MediaWikiHooksTest001', function ( &$list ) {
-                       $list[] = 2;
-                       return; // Implicit null
-               } );
-               Hooks::register( 'MediaWikiHooksTest001', function ( &$list ) {
-                       $list[] = 3;
-                       // No return
-               } );
-
-               Hooks::runWithoutAbort( 'MediaWikiHooksTest001', [ &$list ] );
-               $this->assertSame( [ 1, 2, 3 ], $list, 'All hooks ran.' );
-       }
-
-       /**
-        * @covers Hooks::runWithoutAbort
-        * @covers Hooks::callHook
-        */
-       public function testRunWithoutAbortWarning() {
-               Hooks::register( 'MediaWikiHooksTest001', function ( &$foo ) {
-                       return false;
-               } );
-               Hooks::register( 'MediaWikiHooksTest001', function ( &$foo ) {
-                       $foo = 'test';
-                       return true;
-               } );
-               $foo = 'original';
-
-               $this->setExpectedException(
-                       UnexpectedValueException::class,
-                       'Invalid return from hook-MediaWikiHooksTest001-closure for ' .
-                               'unabortable MediaWikiHooksTest001'
-               );
-               Hooks::runWithoutAbort( 'MediaWikiHooksTest001', [ &$foo ] );
-       }
-
-       /**
-        * @expectedException FatalError
-        * @covers Hooks::run
-        */
-       public function testFatalError() {
-               Hooks::register( 'MediaWikiHooksTest001', function () {
-                       return 'test';
-               } );
-               Hooks::run( 'MediaWikiHooksTest001', [] );
-       }
-}
-
-function NothingFunction( &$foo, &$bar ) {
-       $foo = 'changed-func';
-
-       return true;
-}
-
-function NothingFunctionData( $data, &$foo, &$bar ) {
-       $foo = $data;
-
-       return true;
-}
-
-class NothingClass {
-       public $calls = 0;
-
-       public static function someStatic( &$foo, &$bar ) {
-               $foo = 'changed-static';
-
-               return true;
-       }
-
-       public function someNonStatic( &$foo, &$bar ) {
-               $this->calls++;
-               $foo = 'changed-nonstatic';
-               $bar = 'changed-nonstatic';
-
-               return true;
-       }
-
-       public function onMediaWikiHooksTest001( &$foo, &$bar ) {
-               $this->calls++;
-               $foo = 'changed-onevent';
-
-               return true;
-       }
-
-       public function someNonStaticWithData( $data, &$foo, &$bar ) {
-               $this->calls++;
-               $foo = $data;
-
-               return true;
-       }
-}
diff --git a/tests/phpunit/includes/LicensesTest.php b/tests/phpunit/includes/LicensesTest.php
deleted file mode 100644 (file)
index 0e96bf4..0000000
+++ /dev/null
@@ -1,25 +0,0 @@
-<?php
-
-/**
- * @covers Licenses
- */
-class LicensesTest extends MediaWikiTestCase {
-
-       public function testLicenses() {
-               $str = "
-* Free licenses:
-** GFDL|Debian disagrees
-";
-
-               $lc = new Licenses( [
-                       'fieldname' => 'FooField',
-                       'type' => 'select',
-                       'section' => 'description',
-                       'id' => 'wpLicense',
-                       'label' => 'A label text', # Note can't test label-message because $wgOut is not defined
-                       'name' => 'AnotherName',
-                       'licenses' => $str,
-               ] );
-               $this->assertThat( $lc, $this->isInstanceOf( Licenses::class ) );
-       }
-}
diff --git a/tests/phpunit/includes/ListToggleTest.php b/tests/phpunit/includes/ListToggleTest.php
deleted file mode 100644 (file)
index 3574545..0000000
+++ /dev/null
@@ -1,49 +0,0 @@
-<?php
-
-/**
- * @covers ListToggle
- */
-class ListToggleTest extends MediaWikiTestCase {
-
-       /**
-        * @covers ListToggle::__construct
-        */
-       public function testConstruct() {
-               $output = $this->getMockBuilder( OutputPage::class )
-                       ->setMethods( null )
-                       ->disableOriginalConstructor()
-                       ->getMock();
-
-               $listToggle = new ListToggle( $output );
-
-               $this->assertInstanceOf( ListToggle::class, $listToggle );
-               $this->assertContains( 'mediawiki.checkboxtoggle', $output->getModules() );
-               $this->assertContains( 'mediawiki.checkboxtoggle.styles', $output->getModuleStyles() );
-       }
-
-       /**
-        * @covers ListToggle::getHTML
-        */
-       public function testGetHTML() {
-               $output = $this->createMock( OutputPage::class );
-               $output->expects( $this->any() )
-                       ->method( 'msg' )
-                       ->will( $this->returnCallback( function ( $key ) {
-                               return wfMessage( $key )->inLanguage( 'qqx' );
-                       } ) );
-               $output->expects( $this->once() )
-                       ->method( 'getLanguage' )
-                       ->will( $this->returnValue( Language::factory( 'qqx' ) ) );
-
-               $listToggle = new ListToggle( $output );
-
-               $html = $listToggle->getHTML();
-               $this->assertEquals( '<div class="mw-checkbox-toggle-controls">' .
-                       '(checkbox-select: <a class="mw-checkbox-all" role="button"' .
-                       ' tabindex="0">(checkbox-all)</a>(comma-separator)' .
-                       '<a class="mw-checkbox-none" role="button" tabindex="0">' .
-                       '(checkbox-none)</a>(comma-separator)<a class="mw-checkbox-invert" ' .
-                       'role="button" tabindex="0">(checkbox-invert)</a>)</div>',
-                       $html );
-       }
-}
diff --git a/tests/phpunit/includes/MagicWordFactoryTest.php b/tests/phpunit/includes/MagicWordFactoryTest.php
deleted file mode 100644 (file)
index 065024b..0000000
+++ /dev/null
@@ -1,84 +0,0 @@
-<?php
-
-/**
- * @covers \MagicWordFactory
- *
- * @author Derick N. Alangi
- */
-class MagicWordFactoryTest extends MediaWikiTestCase {
-       private function makeMagicWordFactory( Language $contLang = null ) {
-               return new MagicWordFactory( $contLang ?: Language::factory( 'en' ) );
-       }
-
-       public function testGetContentLanguage() {
-               $contLang = Language::factory( 'en' );
-
-               $magicWordFactory = $this->makeMagicWordFactory( $contLang );
-               $magicWordContLang = $magicWordFactory->getContentLanguage();
-
-               $this->assertSame( $contLang, $magicWordContLang );
-       }
-
-       public function testGetMagicWord() {
-               $magicWordIdValid = 'pageid';
-               $magicWordFactory = $this->makeMagicWordFactory();
-               $mwActual = $magicWordFactory->get( $magicWordIdValid );
-               $contLang = $magicWordFactory->getContentLanguage();
-               $expected = new MagicWord( $magicWordIdValid, [ 'PAGEID' ], false, $contLang );
-
-               $this->assertEquals( $expected, $mwActual );
-       }
-
-       public function testGetInvalidMagicWord() {
-               $magicWordFactory = $this->makeMagicWordFactory();
-
-               $this->setExpectedException( MWException::class );
-               \Wikimedia\suppressWarnings();
-               try {
-                       $magicWordFactory->get( 'invalid magic word' );
-               } finally {
-                       \Wikimedia\restoreWarnings();
-               }
-       }
-
-       public function testGetVariableIDs() {
-               $magicWordFactory = $this->makeMagicWordFactory();
-               $varIds = $magicWordFactory->getVariableIDs();
-
-               $this->assertInternalType( 'array', $varIds );
-               $this->assertNotEmpty( $varIds );
-               $this->assertContainsOnly( 'string', $varIds );
-       }
-
-       public function testGetSubstIDs() {
-               $magicWordFactory = $this->makeMagicWordFactory();
-               $substIds = $magicWordFactory->getSubstIDs();
-
-               $this->assertInternalType( 'array', $substIds );
-               $this->assertNotEmpty( $substIds );
-               $this->assertContainsOnly( 'string', $substIds );
-       }
-
-       /**
-        * Test both valid and invalid caching hints paths
-        */
-       public function testGetCacheTTL() {
-               $magicWordFactory = $this->makeMagicWordFactory();
-               $actual = $magicWordFactory->getCacheTTL( 'localday' );
-
-               $this->assertSame( 3600, $actual );
-
-               $actual = $magicWordFactory->getCacheTTL( 'currentmonth' );
-               $this->assertSame( 86400, $actual );
-
-               $actual = $magicWordFactory->getCacheTTL( 'invalid' );
-               $this->assertSame( -1, $actual );
-       }
-
-       public function testGetDoubleUnderscoreArray() {
-               $magicWordFactory = $this->makeMagicWordFactory();
-               $actual = $magicWordFactory->getDoubleUnderscoreArray();
-
-               $this->assertInstanceOf( MagicWordArray::class, $actual );
-       }
-}
diff --git a/tests/phpunit/includes/MediaWikiServicesTest.php b/tests/phpunit/includes/MediaWikiServicesTest.php
deleted file mode 100644 (file)
index 8fa0cd6..0000000
+++ /dev/null
@@ -1,372 +0,0 @@
-<?php
-
-use MediaWiki\MediaWikiServices;
-use Wikimedia\Services\DestructibleService;
-use Wikimedia\Services\SalvageableService;
-use Wikimedia\Services\ServiceDisabledException;
-
-/**
- * @covers MediaWiki\MediaWikiServices
- *
- * @group MediaWiki
- */
-class MediaWikiServicesTest extends MediaWikiTestCase {
-       private $deprecatedServices = [];
-
-       /**
-        * @return Config
-        */
-       private function newTestConfig() {
-               $globalConfig = new GlobalVarConfig();
-
-               $testConfig = new HashConfig();
-               $testConfig->set( 'ServiceWiringFiles', $globalConfig->get( 'ServiceWiringFiles' ) );
-               $testConfig->set( 'ConfigRegistry', $globalConfig->get( 'ConfigRegistry' ) );
-
-               return $testConfig;
-       }
-
-       /**
-        * @return MediaWikiServices
-        */
-       private function newMediaWikiServices( Config $config = null ) {
-               if ( $config === null ) {
-                       $config = $this->newTestConfig();
-               }
-
-               $instance = new MediaWikiServices( $config );
-
-               // Load the default wiring from the specified files.
-               $wiringFiles = $config->get( 'ServiceWiringFiles' );
-               $instance->loadWiringFiles( $wiringFiles );
-
-               return $instance;
-       }
-
-       public function testGetInstance() {
-               $services = MediaWikiServices::getInstance();
-               $this->assertInstanceOf( MediaWikiServices::class, $services );
-       }
-
-       public function testForceGlobalInstance() {
-               $newServices = $this->newMediaWikiServices();
-               $oldServices = MediaWikiServices::forceGlobalInstance( $newServices );
-
-               $this->assertInstanceOf( MediaWikiServices::class, $oldServices );
-               $this->assertNotSame( $oldServices, $newServices );
-
-               $theServices = MediaWikiServices::getInstance();
-               $this->assertSame( $theServices, $newServices );
-
-               MediaWikiServices::forceGlobalInstance( $oldServices );
-
-               $theServices = MediaWikiServices::getInstance();
-               $this->assertSame( $theServices, $oldServices );
-       }
-
-       public function testResetGlobalInstance() {
-               $newServices = $this->newMediaWikiServices();
-               $oldServices = MediaWikiServices::forceGlobalInstance( $newServices );
-
-               $service1 = $this->createMock( SalvageableService::class );
-               $service1->expects( $this->never() )
-                       ->method( 'salvage' );
-
-               $newServices->defineService(
-                       'Test',
-                       function () use ( $service1 ) {
-                               return $service1;
-                       }
-               );
-
-               // force instantiation
-               $newServices->getService( 'Test' );
-
-               MediaWikiServices::resetGlobalInstance( $this->newTestConfig() );
-               $theServices = MediaWikiServices::getInstance();
-
-               $this->assertSame(
-                       $service1,
-                       $theServices->getService( 'Test' ),
-                       'service definition should survive reset'
-               );
-
-               $this->assertNotSame( $theServices, $newServices );
-               $this->assertNotSame( $theServices, $oldServices );
-
-               MediaWikiServices::forceGlobalInstance( $oldServices );
-       }
-
-       public function testResetGlobalInstance_quick() {
-               $newServices = $this->newMediaWikiServices();
-               $oldServices = MediaWikiServices::forceGlobalInstance( $newServices );
-
-               $service1 = $this->createMock( SalvageableService::class );
-               $service1->expects( $this->never() )
-                       ->method( 'salvage' );
-
-               $service2 = $this->createMock( SalvageableService::class );
-               $service2->expects( $this->once() )
-                       ->method( 'salvage' )
-                       ->with( $service1 );
-
-               // sequence of values the instantiator will return
-               $instantiatorReturnValues = [
-                       $service1,
-                       $service2,
-               ];
-
-               $newServices->defineService(
-                       'Test',
-                       function () use ( &$instantiatorReturnValues ) {
-                               return array_shift( $instantiatorReturnValues );
-                       }
-               );
-
-               // force instantiation
-               $newServices->getService( 'Test' );
-
-               MediaWikiServices::resetGlobalInstance( $this->newTestConfig(), 'quick' );
-               $theServices = MediaWikiServices::getInstance();
-
-               $this->assertSame( $service2, $theServices->getService( 'Test' ) );
-
-               $this->assertNotSame( $theServices, $newServices );
-               $this->assertNotSame( $theServices, $oldServices );
-
-               MediaWikiServices::forceGlobalInstance( $oldServices );
-       }
-
-       public function testDisableStorageBackend() {
-               $newServices = $this->newMediaWikiServices();
-               $oldServices = MediaWikiServices::forceGlobalInstance( $newServices );
-
-               $lbFactory = $this->getMockBuilder( \Wikimedia\Rdbms\LBFactorySimple::class )
-                       ->disableOriginalConstructor()
-                       ->getMock();
-
-               $newServices->redefineService(
-                       'DBLoadBalancerFactory',
-                       function () use ( $lbFactory ) {
-                               return $lbFactory;
-                       }
-               );
-
-               // force the service to become active, so we can check that it does get destroyed
-               $newServices->getService( 'DBLoadBalancerFactory' );
-
-               MediaWikiServices::disableStorageBackend(); // should destroy DBLoadBalancerFactory
-
-               try {
-                       MediaWikiServices::getInstance()->getService( 'DBLoadBalancerFactory' );
-                       $this->fail( 'DBLoadBalancerFactory should have been disabled' );
-               }
-               catch ( ServiceDisabledException $ex ) {
-                       // ok, as expected
-               } catch ( Throwable $ex ) {
-                       $this->fail( 'ServiceDisabledException expected, caught ' . get_class( $ex ) );
-               }
-
-               MediaWikiServices::forceGlobalInstance( $oldServices );
-               $newServices->destroy();
-
-               // No exception was thrown, avoid being risky
-               $this->assertTrue( true );
-       }
-
-       public function testResetChildProcessServices() {
-               $newServices = $this->newMediaWikiServices();
-               $oldServices = MediaWikiServices::forceGlobalInstance( $newServices );
-
-               $service1 = $this->createMock( DestructibleService::class );
-               $service1->expects( $this->once() )
-                       ->method( 'destroy' );
-
-               $service2 = $this->createMock( DestructibleService::class );
-               $service2->expects( $this->never() )
-                       ->method( 'destroy' );
-
-               // sequence of values the instantiator will return
-               $instantiatorReturnValues = [
-                       $service1,
-                       $service2,
-               ];
-
-               $newServices->defineService(
-                       'Test',
-                       function () use ( &$instantiatorReturnValues ) {
-                               return array_shift( $instantiatorReturnValues );
-                       }
-               );
-
-               // force the service to become active, so we can check that it does get destroyed
-               $oldTestService = $newServices->getService( 'Test' );
-
-               MediaWikiServices::resetChildProcessServices();
-               $finalServices = MediaWikiServices::getInstance();
-
-               $newTestService = $finalServices->getService( 'Test' );
-               $this->assertNotSame( $oldTestService, $newTestService );
-
-               MediaWikiServices::forceGlobalInstance( $oldServices );
-       }
-
-       public function testResetServiceForTesting() {
-               $services = $this->newMediaWikiServices();
-               $serviceCounter = 0;
-
-               $services->defineService(
-                       'Test',
-                       function () use ( &$serviceCounter ) {
-                               $serviceCounter++;
-                               $service = $this->createMock( Wikimedia\Services\DestructibleService::class );
-                               $service->expects( $this->once() )->method( 'destroy' );
-                               return $service;
-                       }
-               );
-
-               // This should do nothing. In particular, it should not create a service instance.
-               $services->resetServiceForTesting( 'Test' );
-               $this->assertEquals( 0, $serviceCounter, 'No service instance should be created yet.' );
-
-               $oldInstance = $services->getService( 'Test' );
-               $this->assertEquals( 1, $serviceCounter, 'A service instance should exit now.' );
-
-               // The old instance should be detached, and destroy() called.
-               $services->resetServiceForTesting( 'Test' );
-               $newInstance = $services->getService( 'Test' );
-
-               $this->assertNotSame( $oldInstance, $newInstance );
-
-               // Satisfy the expectation that destroy() is called also for the second service instance.
-               $newInstance->destroy();
-       }
-
-       public function testResetServiceForTesting_noDestroy() {
-               $services = $this->newMediaWikiServices();
-
-               $services->defineService(
-                       'Test',
-                       function () {
-                               $service = $this->createMock( Wikimedia\Services\DestructibleService::class );
-                               $service->expects( $this->never() )->method( 'destroy' );
-                               return $service;
-                       }
-               );
-
-               $oldInstance = $services->getService( 'Test' );
-
-               // The old instance should be detached, but destroy() not called.
-               $services->resetServiceForTesting( 'Test', false );
-               $newInstance = $services->getService( 'Test' );
-
-               $this->assertNotSame( $oldInstance, $newInstance );
-       }
-
-       public function provideGetters() {
-               $getServiceCases = $this->provideGetService();
-               $getterCases = [];
-
-               // All getters should be named just like the service, with "get" added.
-               foreach ( $getServiceCases as $name => $case ) {
-                       if ( $name[0] === '_' ) {
-                               // Internal service, no getter
-                               continue;
-                       }
-                       list( $service, $class ) = $case;
-                       $getterCases[$name] = [
-                               'get' . $service,
-                               $class,
-                               in_array( $service, $this->deprecatedServices )
-                       ];
-               }
-
-               return $getterCases;
-       }
-
-       /**
-        * @dataProvider provideGetters
-        */
-       public function testGetters( $getter, $type, $isDeprecated = false ) {
-               if ( $isDeprecated ) {
-                       $this->hideDeprecated( MediaWikiServices::class . "::$getter" );
-               }
-
-               // Test against the default instance, since the dummy will not know the default services.
-               $services = MediaWikiServices::getInstance();
-               $service = $services->$getter();
-               $this->assertInstanceOf( $type, $service );
-       }
-
-       public function provideGetService() {
-               global $IP;
-               $serviceList = require "$IP/includes/ServiceWiring.php";
-               $ret = [];
-               foreach ( $serviceList as $name => $callback ) {
-                       $fun = new ReflectionFunction( $callback );
-                       if ( !$fun->hasReturnType() ) {
-                               throw new MWException( 'All service callbacks must have a return type defined, ' .
-                                       "none found for $name" );
-                       }
-                       $ret[$name] = [ $name, $fun->getReturnType()->__toString() ];
-               }
-               return $ret;
-       }
-
-       /**
-        * @dataProvider provideGetService
-        */
-       public function testGetService( $name, $type ) {
-               // Test against the default instance, since the dummy will not know the default services.
-               $services = MediaWikiServices::getInstance();
-
-               $service = $services->getService( $name );
-               $this->assertInstanceOf( $type, $service );
-       }
-
-       public function testDefaultServiceInstantiation() {
-               // Check all services in the default instance, not a dummy instance!
-               // Note that we instantiate all services here, including any that
-               // were registered by extensions.
-               $services = MediaWikiServices::getInstance();
-               $names = $services->getServiceNames();
-
-               foreach ( $names as $name ) {
-                       $this->assertTrue( $services->hasService( $name ) );
-                       $service = $services->getService( $name );
-                       $this->assertInternalType( 'object', $service );
-               }
-       }
-
-       public function testDefaultServiceWiringServicesHaveTests() {
-               global $IP;
-               $testedServices = array_keys( $this->provideGetService() );
-               $allServices = array_keys( require "$IP/includes/ServiceWiring.php" );
-               $this->assertEquals(
-                       [],
-                       array_diff( $allServices, $testedServices ),
-                       'The following services have not been added to MediaWikiServicesTest::provideGetService'
-               );
-       }
-
-       public function testGettersAreSorted() {
-               $methods = ( new ReflectionClass( MediaWikiServices::class ) )
-                       ->getMethods( ReflectionMethod::IS_STATIC | ReflectionMethod::IS_PUBLIC );
-
-               $names = array_map( function ( $method ) {
-                       return $method->getName();
-               }, $methods );
-               $serviceNames = array_map( function ( $name ) {
-                       return "get$name";
-               }, array_keys( $this->provideGetService() ) );
-               $names = array_values( array_filter( $names, function ( $name ) use ( $serviceNames ) {
-                       return in_array( $name, $serviceNames );
-               } ) );
-
-               $sortedNames = $names;
-               natcasesort( $sortedNames );
-
-               $this->assertSame( $sortedNames, $names,
-                       'Please keep service getters sorted alphabetically' );
-       }
-}
diff --git a/tests/phpunit/includes/MediaWikiVersionFetcherTest.php b/tests/phpunit/includes/MediaWikiVersionFetcherTest.php
deleted file mode 100644 (file)
index 9803081..0000000
+++ /dev/null
@@ -1,21 +0,0 @@
-<?php
-
-/**
- * Note: this is not a unit test, as it touches the file system and reads an actual file.
- * If unit tests are added for MediaWikiVersionFetcher, this should be done in a distinct test case.
- *
- * @covers MediaWikiVersionFetcher
- *
- * @group ComposerHooks
- *
- * @author Jeroen De Dauw < jeroendedauw@gmail.com >
- */
-class MediaWikiVersionFetcherTest extends MediaWikiTestCase {
-
-       public function testReturnsResult() {
-               global $wgVersion;
-               $versionFetcher = new MediaWikiVersionFetcher();
-               $this->assertSame( $wgVersion, $versionFetcher->fetchVersion() );
-       }
-
-}
diff --git a/tests/phpunit/includes/PathRouterTest.php b/tests/phpunit/includes/PathRouterTest.php
deleted file mode 100644 (file)
index d891675..0000000
+++ /dev/null
@@ -1,325 +0,0 @@
-<?php
-
-/**
- * Tests for the PathRouter parsing.
- *
- * @covers PathRouter
- */
-class PathRouterTest extends MediaWikiTestCase {
-
-       /**
-        * @var PathRouter
-        */
-       protected $basicRouter;
-
-       protected function setUp() {
-               parent::setUp();
-               $router = new PathRouter;
-               $router->add( "/wiki/$1" );
-               $this->basicRouter = $router;
-       }
-
-       public static function provideParse() {
-               $tests = [
-                       // Basic path parsing
-                       'Basic path parsing' => [
-                               "/wiki/$1",
-                               "/wiki/Foo",
-                               [ 'title' => "Foo" ]
-                       ],
-                       //
-                       'Loose path auto-$1: /$1' => [
-                               "/",
-                               "/Foo",
-                               [ 'title' => "Foo" ]
-                       ],
-                       'Loose path auto-$1: /wiki' => [
-                               "/wiki",
-                               "/wiki/Foo",
-                               [ 'title' => "Foo" ]
-                       ],
-                       'Loose path auto-$1: /wiki/' => [
-                               "/wiki/",
-                               "/wiki/Foo",
-                               [ 'title' => "Foo" ]
-                       ],
-                       // Ensure that path is based on specificity, not order
-                       'Order, /$1 added first' => [
-                               [ "/$1", "/a/$1", "/b/$1" ],
-                               "/a/Foo",
-                               [ 'title' => "Foo" ]
-                       ],
-                       'Order, /$1 added last' => [
-                               [ "/b/$1", "/a/$1", "/$1" ],
-                               "/a/Foo",
-                               [ 'title' => "Foo" ]
-                       ],
-                       // Handling of key based arrays with a url parameter
-                       'Key based array' => [
-                               [ [
-                                       'path' => [ 'edit' => "/edit/$1" ],
-                                       'params' => [ 'action' => '$key' ],
-                               ] ],
-                               "/edit/Foo",
-                               [ 'title' => "Foo", 'action' => 'edit' ]
-                       ],
-                       // Additional parameter
-                       'Basic $2' => [
-                               [ [
-                                       'path' => '/$2/$1',
-                                       'params' => [ 'test' => '$2' ]
-                               ] ],
-                               "/asdf/Foo",
-                               [ 'title' => "Foo", 'test' => 'asdf' ]
-                       ],
-               ];
-               // Shared patterns for restricted value parameter tests
-               $restrictedPatterns = [
-                       [
-                               'path' => '/$2/$1',
-                               'params' => [ 'test' => '$2' ],
-                               'options' => [ '$2' => [ 'a', 'b' ] ]
-                       ],
-                       [
-                               'path' => '/$2/$1',
-                               'params' => [ 'test2' => '$2' ],
-                               'options' => [ '$2' => 'c' ]
-                       ],
-                       '/$1'
-               ];
-               $tests += [
-                       // Restricted value parameter tests
-                       'Restricted 1' => [
-                               $restrictedPatterns,
-                               "/asdf/Foo",
-                               [ 'title' => "asdf/Foo" ]
-                       ],
-                       'Restricted 2' => [
-                               $restrictedPatterns,
-                               "/a/Foo",
-                               [ 'title' => "Foo", 'test' => 'a' ]
-                       ],
-                       'Restricted 3' => [
-                               $restrictedPatterns,
-                               "/c/Foo",
-                               [ 'title' => "Foo", 'test2' => 'c' ]
-                       ],
-
-                       // Callback test
-                       'Callback' => [
-                               [ [
-                                       'path' => "/$1",
-                                       'params' => [ 'a' => 'b', 'data:foo' => 'bar' ],
-                                       'options' => [ 'callback' => [ __CLASS__, 'callbackForTest' ] ]
-                               ] ],
-                               '/Foo',
-                               [
-                                       'title' => "Foo",
-                                       'x' => 'Foo',
-                                       'a' => 'b',
-                                       'foo' => 'bar'
-                               ]
-                       ],
-
-                       // Test to ensure that matches are not made if a parameter expects nonexistent input
-                       'Fail' => [
-                               [ [
-                                       'path' => "/wiki/$1",
-                                       'params' => [ 'title' => "$1$2" ],
-                               ] ],
-                               "/wiki/A",
-                               []
-                       ],
-
-                       // Make sure the router handles titles like Special:Recentchanges correctly
-                       'Special title' => [
-                               "/wiki/$1",
-                               "/wiki/Special:Recentchanges",
-                               [ 'title' => "Special:Recentchanges" ]
-                       ],
-
-                       // Make sure the router decodes urlencoding properly
-                       'URL encoding' => [
-                               "/wiki/$1",
-                               "/wiki/Title_With%20Space",
-                               [ 'title' => "Title_With Space" ]
-                       ],
-
-                       // Double slash and dot expansion
-                       'Double slash in prefix' => [
-                               '/wiki/$1',
-                               '//wiki/Foo',
-                               [ 'title' => 'Foo' ]
-                       ],
-                       'Double slash at start of $1' => [
-                               '/wiki/$1',
-                               '/wiki//Foo',
-                               [ 'title' => '/Foo' ]
-                       ],
-                       'Double slash in middle of $1' => [
-                               '/wiki/$1',
-                               '/wiki/.hack//SIGN',
-                               [ 'title' => '.hack//SIGN' ]
-                       ],
-                       'Dots removed 1' => [
-                               '/wiki/$1',
-                               '/x/../wiki/Foo',
-                               [ 'title' => 'Foo' ]
-                       ],
-                       'Dots removed 2' => [
-                               '/wiki/$1',
-                               '/./wiki/Foo',
-                               [ 'title' => 'Foo' ]
-                       ],
-                       'Dots retained 1' => [
-                               '/wiki/$1',
-                               '/wiki/../wiki/Foo',
-                               [ 'title' => '../wiki/Foo' ]
-                       ],
-                       'Dots retained 2' => [
-                               '/wiki/$1',
-                               '/wiki/./Foo',
-                               [ 'title' => './Foo' ]
-                       ],
-                       'Triple slash' => [
-                               '/wiki/$1',
-                               '///wiki/Foo',
-                               [ 'title' => 'Foo' ]
-                       ],
-                       // '..' only traverses one slash, see e.g. RFC 3986
-                       'Dots traversing double slash 1' => [
-                               '/wiki/$1',
-                               '/a//b/../../wiki/Foo',
-                               []
-                       ],
-                       'Dots traversing double slash 2' => [
-                               '/wiki/$1',
-                               '/a//b/../../../wiki/Foo',
-                               [ 'title' => 'Foo' ]
-                       ],
-               ];
-
-               // Make sure the router doesn't break on special characters like $ used in regexp replacements
-               foreach ( [ "$", "$1", "\\", "\\$1" ] as $char ) {
-                       $tests["Regexp character $char"] = [
-                               "/wiki/$1",
-                               "/wiki/$char",
-                               [ 'title' => "$char" ]
-                       ];
-               }
-
-               $tests += [
-                       // Make sure the router handles characters like +&() properly
-                       "Special characters" => [
-                               "/wiki/$1",
-                               "/wiki/Plus+And&Dollar\\Stuff();[]{}*",
-                               [ 'title' => "Plus+And&Dollar\\Stuff();[]{}*" ],
-                       ],
-
-                       // Make sure the router handles unicode characters correctly
-                       "Unicode 1" => [
-                               "/wiki/$1",
-                               "/wiki/Spécial:Modifications_récentes" ,
-                               [ 'title' => "Spécial:Modifications_récentes" ],
-                       ],
-
-                       "Unicode 2" => [
-                               "/wiki/$1",
-                               "/wiki/Sp%C3%A9cial:Modifications_r%C3%A9centes",
-                               [ 'title' => "Spécial:Modifications_récentes" ],
-                       ]
-               ];
-
-               // Ensure the router doesn't choke on long paths.
-               $lorem = "Lorem_ipsum_dolor_sit_amet,_consectetur_adipisicing_elit,_sed_do_eiusmod_" .
-                       "tempor_incididunt_ut_labore_et_dolore_magna_aliqua._Ut_enim_ad_minim_veniam,_quis_" .
-                        "nostrud_exercitation_ullamco_laboris_nisi_ut_aliquip_ex_ea_commodo_consequat._" .
-                        "Duis_aute_irure_dolor_in_reprehenderit_in_voluptate_velit_esse_cillum_dolore_" .
-                        "eu_fugiat_nulla_pariatur._Excepteur_sint_occaecat_cupidatat_non_proident,_sunt_" .
-                        "in_culpa_qui_officia_deserunt_mollit_anim_id_est_laborum.";
-
-               $tests += [
-                       "Long path" => [
-                               "/wiki/$1",
-                               "/wiki/$lorem",
-                               [ 'title' => $lorem ]
-                       ],
-
-                       // Ensure that the php passed site of parameter values are not urldecoded
-                       "Pattern urlencoding" => [
-                               [ [ 'path' => "/wiki/$1", 'params' => [ 'title' => '%20:$1' ] ] ],
-                               "/wiki/Foo",
-                               [ 'title' => '%20:Foo' ]
-                       ],
-
-                       // Ensure that raw parameter values do not have any variable replacements or urldecoding
-                       "Raw param value" => [
-                               [ [ 'path' => "/wiki/$1", 'params' => [ 'title' => [ 'value' => 'bar%20$1' ] ] ] ],
-                               "/wiki/Foo",
-                               [ 'title' => 'bar%20$1' ]
-                       ]
-               ];
-
-               return $tests;
-       }
-
-       /**
-        * Test path parsing
-        * @dataProvider provideParse
-        */
-       public function testParse( $patterns, $path, $expected ) {
-               $patterns = (array)$patterns;
-
-               $router = new PathRouter;
-               foreach ( $patterns as $pattern ) {
-                       if ( is_array( $pattern ) ) {
-                               $router->add( $pattern['path'], $pattern['params'] ?? [],
-                                       $pattern['options'] ?? [] );
-                       } else {
-                               $router->add( $pattern );
-                       }
-               }
-               $matches = $router->parse( $path );
-               $this->assertEquals( $matches, $expected );
-       }
-
-       public static function callbackForTest( &$matches, $data ) {
-               $matches['x'] = $data['$1'];
-               $matches['foo'] = $data['foo'];
-       }
-
-       public static function provideWeight() {
-               return [
-                       [ '/Foo', [ 'title' => 'Foo' ] ],
-                       [ '/Bar', [ 'ping' => 'pong' ] ],
-                       [ '/Baz', [ 'marco' => 'polo' ] ],
-                       [ '/asdf-foo', [ 'title' => 'qwerty-foo' ] ],
-                       [ '/qwerty-bar', [ 'title' => 'asdf-bar' ] ],
-                       [ '/a/Foo', [ 'title' => 'Foo' ] ],
-                       [ '/asdf/Foo', [ 'title' => 'Foo' ] ],
-                       [ '/qwerty/Foo', [ 'title' => 'Foo', 'qwerty' => 'qwerty' ] ],
-                       [ '/baz/Foo', [ 'title' => 'Foo', 'unrestricted' => 'baz' ] ],
-                       [ '/y/Foo', [ 'title' => 'Foo', 'restricted-to-y' => 'y' ] ],
-               ];
-       }
-
-       /**
-        * Test to ensure weight of paths is handled correctly
-        * @dataProvider provideWeight
-        */
-       public function testWeight( $path, $expected ) {
-               $router = new PathRouter;
-               $router->addStrict( "/Bar", [ 'ping' => 'pong' ] );
-               $router->add( "/asdf-$1", [ 'title' => 'qwerty-$1' ] );
-               $router->add( "/$1" );
-               $router->add( "/qwerty-$1", [ 'title' => 'asdf-$1' ] );
-               $router->addStrict( "/Baz", [ 'marco' => 'polo' ] );
-               $router->add( "/a/$1" );
-               $router->add( "/asdf/$1" );
-               $router->add( "/$2/$1", [ 'unrestricted' => '$2' ] );
-               $router->add( [ 'qwerty' => "/qwerty/$1" ], [ 'qwerty' => '$key' ] );
-               $router->add( "/$2/$1", [ 'restricted-to-y' => '$2' ], [ '$2' => 'y' ] );
-
-               $this->assertEquals( $router->parse( $path ), $expected );
-       }
-}
diff --git a/tests/phpunit/includes/Revision/FallbackSlotRoleHandlerTest.php b/tests/phpunit/includes/Revision/FallbackSlotRoleHandlerTest.php
deleted file mode 100644 (file)
index aedf292..0000000
+++ /dev/null
@@ -1,75 +0,0 @@
-<?php
-
-namespace MediaWiki\Tests\Revision;
-
-use MediaWiki\Revision\FallbackSlotRoleHandler;
-use MediaWikiTestCase;
-use Title;
-
-/**
- * @covers \MediaWiki\Revision\FallbackSlotRoleHandler
- */
-class FallbackSlotRoleHandlerTest extends MediaWikiTestCase {
-
-       private function makeBlankTitleObject() {
-               /** @var Title $title */
-               $title = $this->getMockBuilder( Title::class )
-                       ->disableOriginalConstructor()
-                       ->getMock();
-
-               return $title;
-       }
-
-       /**
-        * @covers \MediaWiki\Revision\FallbackSlotRoleHandler::__construct
-        * @covers \MediaWiki\Revision\FallbackSlotRoleHandler::getRole()
-        * @covers \MediaWiki\Revision\FallbackSlotRoleHandler::getNameMessageKey()
-        * @covers \MediaWiki\Revision\FallbackSlotRoleHandler::getDefaultModel()
-        * @covers \MediaWiki\Revision\FallbackSlotRoleHandler::getOutputLayoutHints()
-        */
-       public function testConstruction() {
-               $handler = new FallbackSlotRoleHandler( 'foo' );
-               $this->assertSame( 'foo', $handler->getRole() );
-               $this->assertSame( 'slot-name-foo', $handler->getNameMessageKey() );
-
-               $title = $this->makeBlankTitleObject();
-               $this->assertSame( CONTENT_MODEL_TEXT, $handler->getDefaultModel( $title ) );
-
-               $hints = $handler->getOutputLayoutHints();
-               $this->assertArrayHasKey( 'display', $hints );
-               $this->assertArrayHasKey( 'region', $hints );
-               $this->assertArrayHasKey( 'placement', $hints );
-       }
-
-       /**
-        * @covers \MediaWiki\Revision\FallbackSlotRoleHandler::isAllowedModel()
-        */
-       public function testIsAllowedModel() {
-               $handler = new FallbackSlotRoleHandler( 'foo', 'FooModel' );
-
-               // For the fallback handler, no models are allowed
-               $title = $this->makeBlankTitleObject();
-               $this->assertFalse( $handler->isAllowedModel( 'FooModel', $title ) );
-               $this->assertFalse( $handler->isAllowedModel( 'QuaxModel', $title ) );
-       }
-
-       /**
-        * @covers \MediaWiki\Revision\SlotRoleHandler::isAllowedModel()
-        */
-       public function testIsAllowedOn() {
-               $handler = new FallbackSlotRoleHandler( 'foo', 'FooModel' );
-
-               $title = $this->makeBlankTitleObject();
-               $this->assertFalse( $handler->isAllowedOn( $title ) );
-       }
-
-       /**
-        * @covers \MediaWiki\Revision\FallbackSlotRoleHandler::supportsArticleCount()
-        */
-       public function testSupportsArticleCount() {
-               $handler = new FallbackSlotRoleHandler( 'foo', 'FooModel' );
-
-               $this->assertFalse( $handler->supportsArticleCount() );
-       }
-
-}
diff --git a/tests/phpunit/includes/Revision/MainSlotRoleHandlerTest.php b/tests/phpunit/includes/Revision/MainSlotRoleHandlerTest.php
deleted file mode 100644 (file)
index 5e32574..0000000
+++ /dev/null
@@ -1,79 +0,0 @@
-<?php
-
-namespace MediaWiki\Tests\Revision;
-
-use MediaWiki\Revision\MainSlotRoleHandler;
-use MediaWikiTestCase;
-use PHPUnit\Framework\MockObject\MockObject;
-use Title;
-
-/**
- * @covers \MediaWiki\Revision\MainSlotRoleHandler
- */
-class MainSlotRoleHandlerTest extends MediaWikiTestCase {
-
-       private function makeTitleObject( $ns ) {
-               /** @var Title|MockObject $title */
-               $title = $this->getMockBuilder( Title::class )
-                       ->disableOriginalConstructor()
-                       ->getMock();
-
-               $title->method( 'getNamespace' )
-                       ->willReturn( $ns );
-
-               return $title;
-       }
-
-       /**
-        * @covers \MediaWiki\Revision\MainSlotRoleHandler::__construct
-        * @covers \MediaWiki\Revision\MainSlotRoleHandler::getRole()
-        * @covers \MediaWiki\Revision\MainSlotRoleHandler::getNameMessageKey()
-        * @covers \MediaWiki\Revision\MainSlotRoleHandler::getOutputLayoutHints()
-        */
-       public function testConstruction() {
-               $handler = new MainSlotRoleHandler( [] );
-               $this->assertSame( 'main', $handler->getRole() );
-               $this->assertSame( 'slot-name-main', $handler->getNameMessageKey() );
-
-               $hints = $handler->getOutputLayoutHints();
-               $this->assertArrayHasKey( 'display', $hints );
-               $this->assertArrayHasKey( 'region', $hints );
-               $this->assertArrayHasKey( 'placement', $hints );
-       }
-
-       /**
-        * @covers \MediaWiki\Revision\MainSlotRoleHandler::getDefaultModel()
-        */
-       public function testFetDefaultModel() {
-               $handler = new MainSlotRoleHandler( [ 100 => CONTENT_MODEL_TEXT ] );
-
-               // For the main handler, the namespace determins the default model
-               $titleMain = $this->makeTitleObject( NS_MAIN );
-               $this->assertSame( CONTENT_MODEL_WIKITEXT, $handler->getDefaultModel( $titleMain ) );
-
-               $title100 = $this->makeTitleObject( 100 );
-               $this->assertSame( CONTENT_MODEL_TEXT, $handler->getDefaultModel( $title100 ) );
-       }
-
-       /**
-        * @covers \MediaWiki\Revision\MainSlotRoleHandler::isAllowedModel()
-        */
-       public function testIsAllowedModel() {
-               $handler = new MainSlotRoleHandler( [] );
-
-               // For the main handler, (nearly) all models are allowed
-               $title = $this->makeTitleObject( NS_MAIN );
-               $this->assertTrue( $handler->isAllowedModel( CONTENT_MODEL_WIKITEXT, $title ) );
-               $this->assertTrue( $handler->isAllowedModel( CONTENT_MODEL_TEXT, $title ) );
-       }
-
-       /**
-        * @covers \MediaWiki\Revision\MainSlotRoleHandler::supportsArticleCount()
-        */
-       public function testSupportsArticleCount() {
-               $handler = new MainSlotRoleHandler( [] );
-
-               $this->assertTrue( $handler->supportsArticleCount() );
-       }
-
-}
diff --git a/tests/phpunit/includes/Revision/RevisionStoreFactoryTest.php b/tests/phpunit/includes/Revision/RevisionStoreFactoryTest.php
deleted file mode 100644 (file)
index 138d6bc..0000000
+++ /dev/null
@@ -1,197 +0,0 @@
-<?php
-
-namespace MediaWiki\Tests\Revision;
-
-use ActorMigration;
-use CommentStore;
-use MediaWiki\Logger\Spi as LoggerSpi;
-use MediaWiki\Revision\RevisionStore;
-use MediaWiki\Revision\RevisionStoreFactory;
-use MediaWiki\Revision\SlotRoleRegistry;
-use MediaWiki\Storage\BlobStore;
-use MediaWiki\Storage\BlobStoreFactory;
-use MediaWiki\Storage\NameTableStore;
-use MediaWiki\Storage\NameTableStoreFactory;
-use MediaWiki\Storage\SqlBlobStore;
-use MediaWikiTestCase;
-use Psr\Log\LoggerInterface;
-use Psr\Log\NullLogger;
-use WANObjectCache;
-use Wikimedia\Rdbms\ILBFactory;
-use Wikimedia\Rdbms\ILoadBalancer;
-use Wikimedia\TestingAccessWrapper;
-
-class RevisionStoreFactoryTest extends MediaWikiTestCase {
-
-       /**
-        * @covers \MediaWiki\Revision\RevisionStoreFactory::__construct
-        */
-       public function testValidConstruction_doesntCauseErrors() {
-               new RevisionStoreFactory(
-                       $this->getMockLoadBalancerFactory(),
-                       $this->getMockBlobStoreFactory(),
-                       $this->getNameTableStoreFactory(),
-                       $this->getMockSlotRoleRegistry(),
-                       $this->getHashWANObjectCache(),
-                       $this->getMockCommentStore(),
-                       ActorMigration::newMigration(),
-                       MIGRATION_OLD,
-                       $this->getMockLoggerSpi(),
-                       true
-               );
-               $this->assertTrue( true );
-       }
-
-       public function provideWikiIds() {
-               yield [ true ];
-               yield [ false ];
-               yield [ 'somewiki' ];
-               yield [ 'somewiki', MIGRATION_OLD , false ];
-               yield [ 'somewiki', MIGRATION_NEW , true ];
-       }
-
-       /**
-        * @dataProvider provideWikiIds
-        * @covers \MediaWiki\Revision\RevisionStoreFactory::getRevisionStore
-        */
-       public function testGetRevisionStore(
-               $wikiId,
-               $mcrMigrationStage = MIGRATION_OLD,
-               $contentHandlerUseDb = true
-       ) {
-               $lbFactory = $this->getMockLoadBalancerFactory();
-               $blobStoreFactory = $this->getMockBlobStoreFactory();
-               $nameTableStoreFactory = $this->getNameTableStoreFactory();
-               $slotRoleRegistry = $this->getMockSlotRoleRegistry();
-               $cache = $this->getHashWANObjectCache();
-               $commentStore = $this->getMockCommentStore();
-               $actorMigration = ActorMigration::newMigration();
-               $loggerProvider = $this->getMockLoggerSpi();
-
-               $factory = new RevisionStoreFactory(
-                       $lbFactory,
-                       $blobStoreFactory,
-                       $nameTableStoreFactory,
-                       $slotRoleRegistry,
-                       $cache,
-                       $commentStore,
-                       $actorMigration,
-                       $mcrMigrationStage,
-                       $loggerProvider,
-                       $contentHandlerUseDb
-               );
-
-               $store = $factory->getRevisionStore( $wikiId );
-               $wrapper = TestingAccessWrapper::newFromObject( $store );
-
-               // ensure the correct object type is returned
-               $this->assertInstanceOf( RevisionStore::class, $store );
-
-               // ensure the RevisionStore is for the given wikiId
-               $this->assertSame( $wikiId, $wrapper->wikiId );
-
-               // ensure all other required services are correctly set
-               $this->assertSame( $cache, $wrapper->cache );
-               $this->assertSame( $commentStore, $wrapper->commentStore );
-               $this->assertSame( $mcrMigrationStage, $wrapper->mcrMigrationStage );
-               $this->assertSame( $actorMigration, $wrapper->actorMigration );
-               $this->assertSame( $contentHandlerUseDb, $store->getContentHandlerUseDB() );
-
-               $this->assertInstanceOf( ILoadBalancer::class, $wrapper->loadBalancer );
-               $this->assertInstanceOf( BlobStore::class, $wrapper->blobStore );
-               $this->assertInstanceOf( NameTableStore::class, $wrapper->contentModelStore );
-               $this->assertInstanceOf( NameTableStore::class, $wrapper->slotRoleStore );
-               $this->assertInstanceOf( LoggerInterface::class, $wrapper->logger );
-       }
-
-       /**
-        * @return \PHPUnit_Framework_MockObject_MockObject|ILoadBalancer
-        */
-       private function getMockLoadBalancer() {
-               return $this->getMockBuilder( ILoadBalancer::class )
-                       ->disableOriginalConstructor()->getMock();
-       }
-
-       /**
-        * @return \PHPUnit_Framework_MockObject_MockObject|ILBFactory
-        */
-       private function getMockLoadBalancerFactory() {
-               $mock = $this->getMockBuilder( ILBFactory::class )
-                       ->disableOriginalConstructor()->getMock();
-
-               $mock->method( 'getMainLB' )
-                       ->willReturnCallback( function () {
-                               return $this->getMockLoadBalancer();
-                       } );
-
-               return $mock;
-       }
-
-       /**
-        * @return \PHPUnit_Framework_MockObject_MockObject|SqlBlobStore
-        */
-       private function getMockSqlBlobStore() {
-               return $this->getMockBuilder( SqlBlobStore::class )
-                       ->disableOriginalConstructor()->getMock();
-       }
-
-       /**
-        * @return \PHPUnit_Framework_MockObject_MockObject|BlobStoreFactory
-        */
-       private function getMockBlobStoreFactory() {
-               $mock = $this->getMockBuilder( BlobStoreFactory::class )
-                       ->disableOriginalConstructor()->getMock();
-
-               $mock->method( 'newSqlBlobStore' )
-                       ->willReturnCallback( function () {
-                               return $this->getMockSqlBlobStore();
-                       } );
-
-               return $mock;
-       }
-
-       /**
-        * @return \PHPUnit_Framework_MockObject_MockObject|SlotRoleRegistry
-        */
-       private function getMockSlotRoleRegistry() {
-               $mock = $this->getMockBuilder( SlotRoleRegistry::class )
-                       ->disableOriginalConstructor()->getMock();
-
-               return $mock;
-       }
-
-       /**
-        * @return NameTableStoreFactory
-        */
-       private function getNameTableStoreFactory() {
-               return new NameTableStoreFactory(
-                       $this->getMockLoadBalancerFactory(),
-                       $this->getHashWANObjectCache(),
-                       new NullLogger() );
-       }
-
-       /**
-        * @return \PHPUnit_Framework_MockObject_MockObject|CommentStore
-        */
-       private function getMockCommentStore() {
-               return $this->getMockBuilder( CommentStore::class )
-                       ->disableOriginalConstructor()->getMock();
-       }
-
-       private function getHashWANObjectCache() {
-               return new WANObjectCache( [ 'cache' => new \HashBagOStuff() ] );
-       }
-
-       /**
-        * @return \PHPUnit_Framework_MockObject_MockObject|LoggerSpi
-        */
-       private function getMockLoggerSpi() {
-               $mock = $this->getMock( LoggerSpi::class );
-
-               $mock->method( 'getLogger' )
-                       ->willReturn( new NullLogger() );
-
-               return $mock;
-       }
-
-}
diff --git a/tests/phpunit/includes/Revision/SlotRecordTest.php b/tests/phpunit/includes/Revision/SlotRecordTest.php
deleted file mode 100644 (file)
index 1b6ff2a..0000000
+++ /dev/null
@@ -1,408 +0,0 @@
-<?php
-
-namespace MediaWiki\Tests\Revision;
-
-use InvalidArgumentException;
-use LogicException;
-use MediaWiki\Revision\IncompleteRevisionException;
-use MediaWiki\Revision\SlotRecord;
-use MediaWiki\Revision\SuppressedDataException;
-use MediaWikiTestCase;
-use WikitextContent;
-
-/**
- * @covers \MediaWiki\Revision\SlotRecord
- */
-class SlotRecordTest extends MediaWikiTestCase {
-
-       private function makeRow( $data = [] ) {
-               $data = $data + [
-                       'slot_id' => 1234,
-                       'slot_content_id' => 33,
-                       'content_size' => '5',
-                       'content_sha1' => 'someHash',
-                       'content_address' => 'tt:456',
-                       'model_name' => CONTENT_MODEL_WIKITEXT,
-                       'format_name' => CONTENT_FORMAT_WIKITEXT,
-                       'slot_revision_id' => '2',
-                       'slot_origin' => '1',
-                       'role_name' => 'myRole',
-               ];
-               return (object)$data;
-       }
-
-       public function testCompleteConstruction() {
-               $row = $this->makeRow();
-               $record = new SlotRecord( $row, new WikitextContent( 'A' ) );
-
-               $this->assertTrue( $record->hasAddress() );
-               $this->assertTrue( $record->hasContentId() );
-               $this->assertTrue( $record->hasRevision() );
-               $this->assertTrue( $record->isInherited() );
-               $this->assertSame( 'A', $record->getContent()->getText() );
-               $this->assertSame( 5, $record->getSize() );
-               $this->assertSame( 'someHash', $record->getSha1() );
-               $this->assertSame( CONTENT_MODEL_WIKITEXT, $record->getModel() );
-               $this->assertSame( 2, $record->getRevision() );
-               $this->assertSame( 1, $record->getOrigin() );
-               $this->assertSame( 'tt:456', $record->getAddress() );
-               $this->assertSame( 33, $record->getContentId() );
-               $this->assertSame( CONTENT_FORMAT_WIKITEXT, $record->getFormat() );
-               $this->assertSame( 'myRole', $record->getRole() );
-       }
-
-       public function testConstructionDeferred() {
-               $row = $this->makeRow( [
-                       'content_size' => null, // to be computed
-                       'content_sha1' => null, // to be computed
-                       'format_name' => function () {
-                               return CONTENT_FORMAT_WIKITEXT;
-                       },
-                       'slot_revision_id' => '2',
-                       'slot_origin' => '2',
-                       'slot_content_id' => function () {
-                               return null;
-                       },
-               ] );
-
-               $content = function () {
-                       return new WikitextContent( 'A' );
-               };
-
-               $record = new SlotRecord( $row, $content );
-
-               $this->assertTrue( $record->hasAddress() );
-               $this->assertTrue( $record->hasRevision() );
-               $this->assertFalse( $record->hasContentId() );
-               $this->assertFalse( $record->isInherited() );
-               $this->assertSame( 'A', $record->getContent()->getText() );
-               $this->assertSame( 1, $record->getSize() );
-               $this->assertNotNull( $record->getSha1() );
-               $this->assertSame( CONTENT_MODEL_WIKITEXT, $record->getModel() );
-               $this->assertSame( 2, $record->getRevision() );
-               $this->assertSame( 2, $record->getRevision() );
-               $this->assertSame( 'tt:456', $record->getAddress() );
-               $this->assertSame( CONTENT_FORMAT_WIKITEXT, $record->getFormat() );
-               $this->assertSame( 'myRole', $record->getRole() );
-       }
-
-       public function testNewUnsaved() {
-               $record = SlotRecord::newUnsaved( 'myRole', new WikitextContent( 'A' ) );
-
-               $this->assertFalse( $record->hasAddress() );
-               $this->assertFalse( $record->hasContentId() );
-               $this->assertFalse( $record->hasRevision() );
-               $this->assertFalse( $record->isInherited() );
-               $this->assertFalse( $record->hasOrigin() );
-               $this->assertSame( 'A', $record->getContent()->getText() );
-               $this->assertSame( 1, $record->getSize() );
-               $this->assertNotNull( $record->getSha1() );
-               $this->assertSame( CONTENT_MODEL_WIKITEXT, $record->getModel() );
-               $this->assertSame( 'myRole', $record->getRole() );
-       }
-
-       public function provideInvalidConstruction() {
-               yield 'both null' => [ null, null ];
-               yield 'null row' => [ null, new WikitextContent( 'A' ) ];
-               yield 'array row' => [ [], new WikitextContent( 'A' ) ];
-               yield 'empty row' => [ (object)[], new WikitextContent( 'A' ) ];
-               yield 'null content' => [ (object)[], null ];
-       }
-
-       /**
-        * @dataProvider provideInvalidConstruction
-        */
-       public function testInvalidConstruction( $row, $content ) {
-               $this->setExpectedException( InvalidArgumentException::class );
-               new SlotRecord( $row, $content );
-       }
-
-       public function testGetContentId_fails() {
-               $record = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
-               $this->setExpectedException( IncompleteRevisionException::class );
-
-               $record->getContentId();
-       }
-
-       public function testGetAddress_fails() {
-               $record = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
-               $this->setExpectedException( IncompleteRevisionException::class );
-
-               $record->getAddress();
-       }
-
-       public function provideIncomplete() {
-               $unsaved = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
-               yield 'unsaved' => [ $unsaved ];
-
-               $parent = new SlotRecord( $this->makeRow(), new WikitextContent( 'A' ) );
-               $inherited = SlotRecord::newInherited( $parent );
-               yield 'inherited' => [ $inherited ];
-       }
-
-       /**
-        * @dataProvider provideIncomplete
-        */
-       public function testGetRevision_fails( SlotRecord $record ) {
-               $record = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
-               $this->setExpectedException( IncompleteRevisionException::class );
-
-               $record->getRevision();
-       }
-
-       /**
-        * @dataProvider provideIncomplete
-        */
-       public function testGetOrigin_fails( SlotRecord $record ) {
-               $record = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
-               $this->setExpectedException( IncompleteRevisionException::class );
-
-               $record->getOrigin();
-       }
-
-       public function provideHashStability() {
-               yield [ '', 'phoiac9h4m842xq45sp7s6u21eteeq1' ];
-               yield [ 'Lorem ipsum', 'hcr5u40uxr81d3nx89nvwzclfz6r9c5' ];
-       }
-
-       /**
-        * @dataProvider provideHashStability
-        */
-       public function testHashStability( $text, $hash ) {
-               // Changing the output of the hash function will break things horribly!
-
-               $this->assertSame( $hash, SlotRecord::base36Sha1( $text ) );
-
-               $record = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( $text ) );
-               $this->assertSame( $hash, $record->getSha1() );
-       }
-
-       public function testNewWithSuppressedContent() {
-               $input = new SlotRecord( $this->makeRow(), new WikitextContent( 'A' ) );
-               $output = SlotRecord::newWithSuppressedContent( $input );
-
-               $this->setExpectedException( SuppressedDataException::class );
-               $output->getContent();
-       }
-
-       public function testNewInherited() {
-               $row = $this->makeRow( [ 'slot_revision_id' => 7, 'slot_origin' => 7 ] );
-               $parent = new SlotRecord( $row, new WikitextContent( 'A' ) );
-
-               // This would happen while doing an edit, before saving revision meta-data.
-               $inherited = SlotRecord::newInherited( $parent );
-
-               $this->assertSame( $parent->getContentId(), $inherited->getContentId() );
-               $this->assertSame( $parent->getAddress(), $inherited->getAddress() );
-               $this->assertSame( $parent->getContent(), $inherited->getContent() );
-               $this->assertTrue( $inherited->isInherited() );
-               $this->assertTrue( $inherited->hasOrigin() );
-               $this->assertFalse( $inherited->hasRevision() );
-
-               // make sure we didn't mess with the internal state of $parent
-               $this->assertFalse( $parent->isInherited() );
-               $this->assertSame( 7, $parent->getRevision() );
-
-               // This would happen while doing an edit, after saving the revision meta-data
-               // and content meta-data.
-               $saved = SlotRecord::newSaved(
-                       10,
-                       $inherited->getContentId(),
-                       $inherited->getAddress(),
-                       $inherited
-               );
-               $this->assertSame( $parent->getContentId(), $saved->getContentId() );
-               $this->assertSame( $parent->getAddress(), $saved->getAddress() );
-               $this->assertSame( $parent->getContent(), $saved->getContent() );
-               $this->assertTrue( $saved->isInherited() );
-               $this->assertTrue( $saved->hasRevision() );
-               $this->assertSame( 10, $saved->getRevision() );
-
-               // make sure we didn't mess with the internal state of $parent or $inherited
-               $this->assertSame( 7, $parent->getRevision() );
-               $this->assertFalse( $inherited->hasRevision() );
-       }
-
-       public function testNewSaved() {
-               // This would happen while doing an edit, before saving revision meta-data.
-               $unsaved = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
-
-               // This would happen while doing an edit, after saving the revision meta-data
-               // and content meta-data.
-               $saved = SlotRecord::newSaved( 10, 20, 'theNewAddress', $unsaved );
-               $this->assertFalse( $saved->isInherited() );
-               $this->assertTrue( $saved->hasOrigin() );
-               $this->assertTrue( $saved->hasRevision() );
-               $this->assertTrue( $saved->hasAddress() );
-               $this->assertTrue( $saved->hasContentId() );
-               $this->assertSame( 'theNewAddress', $saved->getAddress() );
-               $this->assertSame( 20, $saved->getContentId() );
-               $this->assertSame( 'A', $saved->getContent()->getText() );
-               $this->assertSame( 10, $saved->getRevision() );
-               $this->assertSame( 10, $saved->getOrigin() );
-
-               // make sure we didn't mess with the internal state of $unsaved
-               $this->assertFalse( $unsaved->hasAddress() );
-               $this->assertFalse( $unsaved->hasContentId() );
-               $this->assertFalse( $unsaved->hasRevision() );
-       }
-
-       public function provideNewSaved_LogicException() {
-               $freshRow = $this->makeRow( [
-                       'content_id' => 10,
-                       'content_address' => 'address:1',
-                       'slot_origin' => 1,
-                       'slot_revision_id' => 1,
-               ] );
-
-               $freshSlot = new SlotRecord( $freshRow, new WikitextContent( 'A' ) );
-               yield 'mismatching address' => [ 1, 10, 'address:BAD', $freshSlot ];
-               yield 'mismatching revision' => [ 5, 10, 'address:1', $freshSlot ];
-               yield 'mismatching content ID' => [ 1, 17, 'address:1', $freshSlot ];
-
-               $inheritedRow = $this->makeRow( [
-                       'content_id' => null,
-                       'content_address' => null,
-                       'slot_origin' => 0,
-                       'slot_revision_id' => 1,
-               ] );
-
-               $inheritedSlot = new SlotRecord( $inheritedRow, new WikitextContent( 'A' ) );
-               yield 'inherited, but no address' => [ 1, 10, 'address:2', $inheritedSlot ];
-       }
-
-       /**
-        * @dataProvider provideNewSaved_LogicException
-        */
-       public function testNewSaved_LogicException(
-               $revisionId,
-               $contentId,
-               $contentAddress,
-               SlotRecord $protoSlot
-       ) {
-               $this->setExpectedException( LogicException::class );
-               SlotRecord::newSaved( $revisionId, $contentId, $contentAddress, $protoSlot );
-       }
-
-       public function provideNewSaved_InvalidArgumentException() {
-               $unsaved = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
-
-               yield 'bad revision id' => [ 'xyzzy', 5, 'address', $unsaved ];
-               yield 'bad content id' => [ 7, 'xyzzy', 'address', $unsaved ];
-               yield 'bad content address' => [ 7, 5, 77, $unsaved ];
-       }
-
-       /**
-        * @dataProvider provideNewSaved_InvalidArgumentException
-        */
-       public function testNewSaved_InvalidArgumentException(
-               $revisionId,
-               $contentId,
-               $contentAddress,
-               SlotRecord $protoSlot
-       ) {
-               $this->setExpectedException( InvalidArgumentException::class );
-               SlotRecord::newSaved( $revisionId, $contentId, $contentAddress, $protoSlot );
-       }
-
-       public function provideHasSameContent() {
-               $fail = function () {
-                       self::fail( 'There should be no need to actually load the content.' );
-               };
-
-               $a100a1 = new SlotRecord(
-                       $this->makeRow(
-                               [
-                                       'model_name' => 'A',
-                                       'content_size' => 100,
-                                       'content_sha1' => 'hash-a',
-                                       'content_address' => 'xxx:a1',
-                               ]
-                       ),
-                       $fail
-               );
-               $a100a1b = new SlotRecord(
-                       $this->makeRow(
-                               [
-                                       'model_name' => 'A',
-                                       'content_size' => 100,
-                                       'content_sha1' => 'hash-a',
-                                       'content_address' => 'xxx:a1',
-                               ]
-                       ),
-                       $fail
-               );
-               $a100null = new SlotRecord(
-                       $this->makeRow(
-                               [
-                                       'model_name' => 'A',
-                                       'content_size' => 100,
-                                       'content_sha1' => 'hash-a',
-                                       'content_address' => null,
-                               ]
-                       ),
-                       $fail
-               );
-               $a100a2 = new SlotRecord(
-                       $this->makeRow(
-                               [
-                                       'model_name' => 'A',
-                                       'content_size' => 100,
-                                       'content_sha1' => 'hash-a',
-                                       'content_address' => 'xxx:a2',
-                               ]
-                       ),
-                       $fail
-               );
-               $b100a1 = new SlotRecord(
-                       $this->makeRow(
-                               [
-                                       'model_name' => 'B',
-                                       'content_size' => 100,
-                                       'content_sha1' => 'hash-a',
-                                       'content_address' => 'xxx:a1',
-                               ]
-                       ),
-                       $fail
-               );
-               $a200a1 = new SlotRecord(
-                       $this->makeRow(
-                               [
-                                       'model_name' => 'A',
-                                       'content_size' => 200,
-                                       'content_sha1' => 'hash-a',
-                                       'content_address' => 'xxx:a2',
-                               ]
-                       ),
-                       $fail
-               );
-               $a100x1 = new SlotRecord(
-                       $this->makeRow(
-                               [
-                                       'model_name' => 'A',
-                                       'content_size' => 100,
-                                       'content_sha1' => 'hash-x',
-                                       'content_address' => 'xxx:x1',
-                               ]
-                       ),
-                       $fail
-               );
-
-               yield 'same instance' => [ $a100a1, $a100a1, true ];
-               yield 'no address' => [ $a100a1, $a100null, true ];
-               yield 'same address' => [ $a100a1, $a100a1b, true ];
-               yield 'different address' => [ $a100a1, $a100a2, true ];
-               yield 'different model' => [ $a100a1, $b100a1, false ];
-               yield 'different size' => [ $a100a1, $a200a1, false ];
-               yield 'different hash' => [ $a100a1, $a100x1, false ];
-       }
-
-       /**
-        * @dataProvider provideHasSameContent
-        */
-       public function testHasSameContent( SlotRecord $a, SlotRecord $b, $sameContent ) {
-               $this->assertSame( $sameContent, $a->hasSameContent( $b ) );
-               $this->assertSame( $sameContent, $b->hasSameContent( $a ) );
-       }
-
-}
diff --git a/tests/phpunit/includes/Revision/SlotRoleHandlerTest.php b/tests/phpunit/includes/Revision/SlotRoleHandlerTest.php
deleted file mode 100644 (file)
index 67e9464..0000000
+++ /dev/null
@@ -1,67 +0,0 @@
-<?php
-
-namespace MediaWiki\Tests\Revision;
-
-use MediaWiki\Revision\SlotRoleHandler;
-use MediaWikiTestCase;
-use Title;
-
-/**
- * @covers \MediaWiki\Revision\SlotRoleHandler
- */
-class SlotRoleHandlerTest extends MediaWikiTestCase {
-
-       private function makeBlankTitleObject() {
-               /** @var Title $title */
-               $title = $this->getMockBuilder( Title::class )
-                       ->disableOriginalConstructor()
-                       ->getMock();
-
-               return $title;
-       }
-
-       /**
-        * @covers \MediaWiki\Revision\SlotRoleHandler::__construct
-        * @covers \MediaWiki\Revision\SlotRoleHandler::getRole()
-        * @covers \MediaWiki\Revision\SlotRoleHandler::getNameMessageKey()
-        * @covers \MediaWiki\Revision\SlotRoleHandler::getDefaultModel()
-        * @covers \MediaWiki\Revision\SlotRoleHandler::getOutputLayoutHints()
-        */
-       public function testConstruction() {
-               $handler = new SlotRoleHandler( 'foo', 'FooModel', [ 'frob' => 'niz' ] );
-               $this->assertSame( 'foo', $handler->getRole() );
-               $this->assertSame( 'slot-name-foo', $handler->getNameMessageKey() );
-
-               $title = $this->makeBlankTitleObject();
-               $this->assertSame( 'FooModel', $handler->getDefaultModel( $title ) );
-
-               $hints = $handler->getOutputLayoutHints();
-               $this->assertArrayHasKey( 'frob', $hints );
-               $this->assertSame( 'niz', $hints['frob'] );
-
-               $this->assertArrayHasKey( 'display', $hints );
-               $this->assertArrayHasKey( 'region', $hints );
-               $this->assertArrayHasKey( 'placement', $hints );
-       }
-
-       /**
-        * @covers \MediaWiki\Revision\SlotRoleHandler::isAllowedModel()
-        */
-       public function testIsAllowedModel() {
-               $handler = new SlotRoleHandler( 'foo', 'FooModel' );
-
-               $title = $this->makeBlankTitleObject();
-               $this->assertTrue( $handler->isAllowedModel( 'FooModel', $title ) );
-               $this->assertFalse( $handler->isAllowedModel( 'QuaxModel', $title ) );
-       }
-
-       /**
-        * @covers \MediaWiki\Revision\SlotRoleHandler::supportsArticleCount()
-        */
-       public function testSupportsArticleCount() {
-               $handler = new SlotRoleHandler( 'foo', 'FooModel' );
-
-               $this->assertFalse( $handler->supportsArticleCount() );
-       }
-
-}
diff --git a/tests/phpunit/includes/SanitizerValidateEmailTest.php b/tests/phpunit/includes/SanitizerValidateEmailTest.php
deleted file mode 100644 (file)
index c4e4308..0000000
+++ /dev/null
@@ -1,105 +0,0 @@
-<?php
-
-/**
- * @covers Sanitizer::validateEmail
- * @todo all test methods in this class should be refactored and...
- *    use a single test method and a single data provider...
- */
-class SanitizerValidateEmailTest extends PHPUnit\Framework\TestCase {
-
-       use MediaWikiCoversValidator;
-
-       private function checkEmail( $addr, $expected = true, $msg = '' ) {
-               if ( $msg == '' ) {
-                       $msg = "Testing $addr";
-               }
-
-               $this->assertEquals(
-                       $expected,
-                       Sanitizer::validateEmail( $addr ),
-                       $msg
-               );
-       }
-
-       private function valid( $addr, $msg = '' ) {
-               $this->checkEmail( $addr, true, $msg );
-       }
-
-       private function invalid( $addr, $msg = '' ) {
-               $this->checkEmail( $addr, false, $msg );
-       }
-
-       public function testEmailWellKnownUserAtHostDotTldAreValid() {
-               $this->valid( 'user@example.com' );
-               $this->valid( 'user@example.museum' );
-       }
-
-       public function testEmailWithUpperCaseCharactersAreValid() {
-               $this->valid( 'USER@example.com' );
-               $this->valid( 'user@EXAMPLE.COM' );
-               $this->valid( 'user@Example.com' );
-               $this->valid( 'USER@eXAMPLE.com' );
-       }
-
-       public function testEmailWithAPlusInUserName() {
-               $this->valid( 'user+sub@example.com' );
-               $this->valid( 'user+@example.com' );
-       }
-
-       public function testEmailDoesNotNeedATopLevelDomain() {
-               $this->valid( "user@localhost" );
-               $this->valid( "FooBar@localdomain" );
-               $this->valid( "nobody@mycompany" );
-       }
-
-       public function testEmailWithWhiteSpacesBeforeOrAfterAreInvalids() {
-               $this->invalid( " user@host.com" );
-               $this->invalid( "user@host.com " );
-               $this->invalid( "\tuser@host.com" );
-               $this->invalid( "user@host.com\t" );
-       }
-
-       public function testEmailWithWhiteSpacesAreInvalids() {
-               $this->invalid( "User user@host" );
-               $this->invalid( "first last@mycompany" );
-               $this->invalid( "firstlast@my company" );
-       }
-
-       /**
-        * T28948 : comma were matched by an incorrect regexp range
-        */
-       public function testEmailWithCommasAreInvalids() {
-               $this->invalid( "user,foo@example.org" );
-               $this->invalid( "userfoo@ex,ample.org" );
-       }
-
-       public function testEmailWithHyphens() {
-               $this->valid( "user-foo@example.org" );
-               $this->valid( "userfoo@ex-ample.org" );
-       }
-
-       public function testEmailDomainCanNotBeginWithDot() {
-               $this->invalid( "user@." );
-               $this->invalid( "user@.localdomain" );
-               $this->invalid( "user@localdomain." );
-               $this->valid( "user.@localdomain" );
-               $this->valid( ".@localdomain" );
-               $this->invalid( ".@a............" );
-       }
-
-       public function testEmailWithFunnyCharacters() {
-               $this->valid( "\$user!ex{this}@123.com" );
-       }
-
-       public function testEmailTopLevelDomainCanBeNumerical() {
-               $this->valid( "user@example.1234" );
-       }
-
-       public function testEmailWithoutAtSignIsInvalid() {
-               $this->invalid( 'useràexample.com' );
-       }
-
-       public function testEmailWithOneCharacterDomainIsValid() {
-               $this->valid( 'user@a' );
-       }
-}
diff --git a/tests/phpunit/includes/ServiceWiringTest.php b/tests/phpunit/includes/ServiceWiringTest.php
deleted file mode 100644 (file)
index 02e06f8..0000000
+++ /dev/null
@@ -1,16 +0,0 @@
-<?php
-
-/**
- * @coversNothing
- */
-class ServiceWiringTest extends MediaWikiTestCase {
-       public function testServicesAreSorted() {
-               global $IP;
-               $services = array_keys( require "$IP/includes/ServiceWiring.php" );
-               $sortedServices = $services;
-               natcasesort( $sortedServices );
-
-               $this->assertSame( $sortedServices, $services,
-                       'Please keep services sorted alphabetically' );
-       }
-}
diff --git a/tests/phpunit/includes/SiteConfigurationTest.php b/tests/phpunit/includes/SiteConfigurationTest.php
deleted file mode 100644 (file)
index 3b72262..0000000
+++ /dev/null
@@ -1,379 +0,0 @@
-<?php
-
-class SiteConfigurationTest extends MediaWikiTestCase {
-
-       /**
-        * @var SiteConfiguration
-        */
-       protected $mConf;
-
-       protected function setUp() {
-               parent::setUp();
-
-               $this->mConf = new SiteConfiguration;
-
-               $this->mConf->suffixes = [ 'wikipedia' => 'wiki' ];
-               $this->mConf->wikis = [ 'enwiki', 'dewiki', 'frwiki' ];
-               $this->mConf->settings = [
-                       'SimpleKey' => [
-                               'wiki' => 'wiki',
-                               'tag' => 'tag',
-                               'enwiki' => 'enwiki',
-                               'dewiki' => 'dewiki',
-                               'frwiki' => 'frwiki',
-                       ],
-
-                       'Fallback' => [
-                               'default' => 'default',
-                               'wiki' => 'wiki',
-                               'tag' => 'tag',
-                               'frwiki' => 'frwiki',
-                               'null_wiki' => null,
-                       ],
-
-                       'WithParams' => [
-                               'default' => '$lang $site $wiki',
-                       ],
-
-                       '+SomeGlobal' => [
-                               'wiki' => [
-                                       'wiki' => 'wiki',
-                               ],
-                               'tag' => [
-                                       'tag' => 'tag',
-                               ],
-                               'enwiki' => [
-                                       'enwiki' => 'enwiki',
-                               ],
-                               'dewiki' => [
-                                       'dewiki' => 'dewiki',
-                               ],
-                               'frwiki' => [
-                                       'frwiki' => 'frwiki',
-                               ],
-                       ],
-
-                       'MergeIt' => [
-                               '+wiki' => [
-                                       'wiki' => 'wiki',
-                               ],
-                               '+tag' => [
-                                       'tag' => 'tag',
-                               ],
-                               'default' => [
-                                       'default' => 'default',
-                               ],
-                               '+enwiki' => [
-                                       'enwiki' => 'enwiki',
-                               ],
-                               '+dewiki' => [
-                                       'dewiki' => 'dewiki',
-                               ],
-                               '+frwiki' => [
-                                       'frwiki' => 'frwiki',
-                               ],
-                       ],
-               ];
-
-               $GLOBALS['SomeGlobal'] = [ 'SomeGlobal' => 'SomeGlobal' ];
-       }
-
-       /**
-        * This function is used as a callback within the tests below
-        */
-       public static function getSiteParamsCallback( $conf, $wiki ) {
-               $site = null;
-               $lang = null;
-               foreach ( $conf->suffixes as $suffix ) {
-                       if ( substr( $wiki, -strlen( $suffix ) ) == $suffix ) {
-                               $site = $suffix;
-                               $lang = substr( $wiki, 0, -strlen( $suffix ) );
-                               break;
-                       }
-               }
-
-               return [
-                       'suffix' => $site,
-                       'lang' => $lang,
-                       'params' => [
-                               'lang' => $lang,
-                               'site' => $site,
-                               'wiki' => $wiki,
-                       ],
-                       'tags' => [ 'tag' ],
-               ];
-       }
-
-       /**
-        * @covers SiteConfiguration::siteFromDB
-        */
-       public function testSiteFromDb() {
-               $this->assertEquals(
-                       [ 'wikipedia', 'en' ],
-                       $this->mConf->siteFromDB( 'enwiki' ),
-                       'siteFromDB()'
-               );
-               $this->assertEquals(
-                       [ 'wikipedia', '' ],
-                       $this->mConf->siteFromDB( 'wiki' ),
-                       'siteFromDB() on a suffix'
-               );
-               $this->assertEquals(
-                       [ null, null ],
-                       $this->mConf->siteFromDB( 'wikien' ),
-                       'siteFromDB() on a non-existing wiki'
-               );
-
-               $this->mConf->suffixes = [ 'wiki', '' ];
-               $this->assertEquals(
-                       [ '', 'wikien' ],
-                       $this->mConf->siteFromDB( 'wikien' ),
-                       'siteFromDB() on a non-existing wiki (2)'
-               );
-       }
-
-       /**
-        * @covers SiteConfiguration::getLocalDatabases
-        */
-       public function testGetLocalDatabases() {
-               $this->assertEquals(
-                       [ 'enwiki', 'dewiki', 'frwiki' ],
-                       $this->mConf->getLocalDatabases(),
-                       'getLocalDatabases()'
-               );
-       }
-
-       /**
-        * @covers SiteConfiguration::get
-        */
-       public function testGetConfVariables() {
-               // Simple
-               $this->assertEquals(
-                       'enwiki',
-                       $this->mConf->get( 'SimpleKey', 'enwiki', 'wiki' ),
-                       'get(): simple setting on an existing wiki'
-               );
-               $this->assertEquals(
-                       'dewiki',
-                       $this->mConf->get( 'SimpleKey', 'dewiki', 'wiki' ),
-                       'get(): simple setting on an existing wiki (2)'
-               );
-               $this->assertEquals(
-                       'frwiki',
-                       $this->mConf->get( 'SimpleKey', 'frwiki', 'wiki' ),
-                       'get(): simple setting on an existing wiki (3)'
-               );
-               $this->assertEquals(
-                       'wiki',
-                       $this->mConf->get( 'SimpleKey', 'wiki', 'wiki' ),
-                       'get(): simple setting on an suffix'
-               );
-               $this->assertEquals(
-                       'wiki',
-                       $this->mConf->get( 'SimpleKey', 'eswiki', 'wiki' ),
-                       'get(): simple setting on an non-existing wiki'
-               );
-
-               // Fallback
-               $this->assertEquals(
-                       'wiki',
-                       $this->mConf->get( 'Fallback', 'enwiki', 'wiki' ),
-                       'get(): fallback setting on an existing wiki'
-               );
-               $this->assertEquals(
-                       'tag',
-                       $this->mConf->get( 'Fallback', 'dewiki', 'wiki', [], [ 'tag' ] ),
-                       'get(): fallback setting on an existing wiki (with wiki tag)'
-               );
-               $this->assertEquals(
-                       'frwiki',
-                       $this->mConf->get( 'Fallback', 'frwiki', 'wiki', [], [ 'tag' ] ),
-                       'get(): no fallback if wiki has its own setting (matching tag)'
-               );
-               $this->assertSame(
-                       // Potential regression test for T192855
-                       null,
-                       $this->mConf->get( 'Fallback', 'null_wiki', 'wiki', [], [ 'tag' ] ),
-                       'get(): no fallback if wiki has its own setting (matching tag and uses null)'
-               );
-               $this->assertEquals(
-                       'wiki',
-                       $this->mConf->get( 'Fallback', 'wiki', 'wiki' ),
-                       'get(): fallback setting on an suffix'
-               );
-               $this->assertEquals(
-                       'wiki',
-                       $this->mConf->get( 'Fallback', 'wiki', 'wiki', [], [ 'tag' ] ),
-                       'get(): fallback setting on an suffix (with wiki tag)'
-               );
-               $this->assertEquals(
-                       'wiki',
-                       $this->mConf->get( 'Fallback', 'eswiki', 'wiki' ),
-                       'get(): fallback setting on an non-existing wiki'
-               );
-               $this->assertEquals(
-                       'tag',
-                       $this->mConf->get( 'Fallback', 'eswiki', 'wiki', [], [ 'tag' ] ),
-                       'get(): fallback setting on an non-existing wiki (with wiki tag)'
-               );
-
-               // Merging
-               $common = [ 'wiki' => 'wiki', 'default' => 'default' ];
-               $commonTag = [ 'tag' => 'tag', 'wiki' => 'wiki', 'default' => 'default' ];
-               $this->assertEquals(
-                       [ 'enwiki' => 'enwiki' ] + $common,
-                       $this->mConf->get( 'MergeIt', 'enwiki', 'wiki' ),
-                       'get(): merging setting on an existing wiki'
-               );
-               $this->assertEquals(
-                       [ 'enwiki' => 'enwiki' ] + $commonTag,
-                       $this->mConf->get( 'MergeIt', 'enwiki', 'wiki', [], [ 'tag' ] ),
-                       'get(): merging setting on an existing wiki (with tag)'
-               );
-               $this->assertEquals(
-                       [ 'dewiki' => 'dewiki' ] + $common,
-                       $this->mConf->get( 'MergeIt', 'dewiki', 'wiki' ),
-                       'get(): merging setting on an existing wiki (2)'
-               );
-               $this->assertEquals(
-                       [ 'dewiki' => 'dewiki' ] + $commonTag,
-                       $this->mConf->get( 'MergeIt', 'dewiki', 'wiki', [], [ 'tag' ] ),
-                       'get(): merging setting on an existing wiki (2) (with tag)'
-               );
-               $this->assertEquals(
-                       [ 'frwiki' => 'frwiki' ] + $common,
-                       $this->mConf->get( 'MergeIt', 'frwiki', 'wiki' ),
-                       'get(): merging setting on an existing wiki (3)'
-               );
-               $this->assertEquals(
-                       [ 'frwiki' => 'frwiki' ] + $commonTag,
-                       $this->mConf->get( 'MergeIt', 'frwiki', 'wiki', [], [ 'tag' ] ),
-                       'get(): merging setting on an existing wiki (3) (with tag)'
-               );
-               $this->assertEquals(
-                       [ 'wiki' => 'wiki' ] + $common,
-                       $this->mConf->get( 'MergeIt', 'wiki', 'wiki' ),
-                       'get(): merging setting on an suffix'
-               );
-               $this->assertEquals(
-                       [ 'wiki' => 'wiki' ] + $commonTag,
-                       $this->mConf->get( 'MergeIt', 'wiki', 'wiki', [], [ 'tag' ] ),
-                       'get(): merging setting on an suffix (with tag)'
-               );
-               $this->assertEquals(
-                       $common,
-                       $this->mConf->get( 'MergeIt', 'eswiki', 'wiki' ),
-                       'get(): merging setting on an non-existing wiki'
-               );
-               $this->assertEquals(
-                       $commonTag,
-                       $this->mConf->get( 'MergeIt', 'eswiki', 'wiki', [], [ 'tag' ] ),
-                       'get(): merging setting on an non-existing wiki (with tag)'
-               );
-       }
-
-       /**
-        * @covers SiteConfiguration::siteFromDB
-        */
-       public function testSiteFromDbWithCallback() {
-               $this->mConf->siteParamsCallback = 'SiteConfigurationTest::getSiteParamsCallback';
-
-               $this->assertEquals(
-                       [ 'wiki', 'en' ],
-                       $this->mConf->siteFromDB( 'enwiki' ),
-                       'siteFromDB() with callback'
-               );
-               $this->assertEquals(
-                       [ 'wiki', '' ],
-                       $this->mConf->siteFromDB( 'wiki' ),
-                       'siteFromDB() with callback on a suffix'
-               );
-               $this->assertEquals(
-                       [ null, null ],
-                       $this->mConf->siteFromDB( 'wikien' ),
-                       'siteFromDB() with callback on a non-existing wiki'
-               );
-       }
-
-       /**
-        * @covers SiteConfiguration::get
-        */
-       public function testParameterReplacement() {
-               $this->mConf->siteParamsCallback = 'SiteConfigurationTest::getSiteParamsCallback';
-
-               $this->assertEquals(
-                       'en wiki enwiki',
-                       $this->mConf->get( 'WithParams', 'enwiki', 'wiki' ),
-                       'get(): parameter replacement on an existing wiki'
-               );
-               $this->assertEquals(
-                       'de wiki dewiki',
-                       $this->mConf->get( 'WithParams', 'dewiki', 'wiki' ),
-                       'get(): parameter replacement on an existing wiki (2)'
-               );
-               $this->assertEquals(
-                       'fr wiki frwiki',
-                       $this->mConf->get( 'WithParams', 'frwiki', 'wiki' ),
-                       'get(): parameter replacement on an existing wiki (3)'
-               );
-               $this->assertEquals(
-                       ' wiki wiki',
-                       $this->mConf->get( 'WithParams', 'wiki', 'wiki' ),
-                       'get(): parameter replacement on an suffix'
-               );
-               $this->assertEquals(
-                       'es wiki eswiki',
-                       $this->mConf->get( 'WithParams', 'eswiki', 'wiki' ),
-                       'get(): parameter replacement on an non-existing wiki'
-               );
-       }
-
-       /**
-        * @covers SiteConfiguration::getAll
-        */
-       public function testGetAllGlobals() {
-               $this->mConf->siteParamsCallback = 'SiteConfigurationTest::getSiteParamsCallback';
-
-               $getall = [
-                       'SimpleKey' => 'enwiki',
-                       'Fallback' => 'tag',
-                       'WithParams' => 'en wiki enwiki',
-                       'SomeGlobal' => [ 'enwiki' => 'enwiki' ] + $GLOBALS['SomeGlobal'],
-                       'MergeIt' => [
-                               'enwiki' => 'enwiki',
-                               'tag' => 'tag',
-                               'wiki' => 'wiki',
-                               'default' => 'default'
-                       ],
-               ];
-               $this->assertEquals( $getall, $this->mConf->getAll( 'enwiki' ), 'getAll()' );
-
-               $this->mConf->extractAllGlobals( 'enwiki', 'wiki' );
-
-               $this->assertEquals(
-                       $getall['SimpleKey'],
-                       $GLOBALS['SimpleKey'],
-                       'extractAllGlobals(): simple setting'
-               );
-               $this->assertEquals(
-                       $getall['Fallback'],
-                       $GLOBALS['Fallback'],
-                       'extractAllGlobals(): fallback setting'
-               );
-               $this->assertEquals(
-                       $getall['WithParams'],
-                       $GLOBALS['WithParams'],
-                       'extractAllGlobals(): parameter replacement'
-               );
-               $this->assertEquals(
-                       $getall['SomeGlobal'],
-                       $GLOBALS['SomeGlobal'],
-                       'extractAllGlobals(): merging with global'
-               );
-               $this->assertEquals(
-                       $getall['MergeIt'],
-                       $GLOBALS['MergeIt'],
-                       'extractAllGlobals(): merging setting'
-               );
-       }
-}
diff --git a/tests/phpunit/includes/Storage/BlobStoreFactoryTest.php b/tests/phpunit/includes/Storage/BlobStoreFactoryTest.php
deleted file mode 100644 (file)
index 252c657..0000000
+++ /dev/null
@@ -1,46 +0,0 @@
-<?php
-
-namespace MediaWiki\Tests\Storage;
-
-use MediaWiki\MediaWikiServices;
-use MediaWiki\Storage\BlobStore;
-use MediaWiki\Storage\SqlBlobStore;
-use MediaWikiTestCase;
-use Wikimedia\TestingAccessWrapper;
-
-/**
- * @covers \MediaWiki\Storage\BlobStoreFactory
- */
-class BlobStoreFactoryTest extends MediaWikiTestCase {
-
-       public function provideWikiIds() {
-               yield [ false ];
-               yield [ 'someWiki' ];
-       }
-
-       /**
-        * @dataProvider provideWikiIds
-        */
-       public function testNewBlobStore( $wikiId ) {
-               $factory = MediaWikiServices::getInstance()->getBlobStoreFactory();
-               $store = $factory->newBlobStore( $wikiId );
-               $this->assertInstanceOf( BlobStore::class, $store );
-
-               // This only works as we currently know this is a SqlBlobStore object
-               $wrapper = TestingAccessWrapper::newFromObject( $store );
-               $this->assertEquals( $wikiId, $wrapper->wikiId );
-       }
-
-       /**
-        * @dataProvider provideWikiIds
-        */
-       public function testNewSqlBlobStore( $wikiId ) {
-               $factory = MediaWikiServices::getInstance()->getBlobStoreFactory();
-               $store = $factory->newSqlBlobStore( $wikiId );
-               $this->assertInstanceOf( SqlBlobStore::class, $store );
-
-               $wrapper = TestingAccessWrapper::newFromObject( $store );
-               $this->assertEquals( $wikiId, $wrapper->wikiId );
-       }
-
-}
diff --git a/tests/phpunit/includes/Storage/PreparedEditTest.php b/tests/phpunit/includes/Storage/PreparedEditTest.php
deleted file mode 100644 (file)
index 29999ee..0000000
+++ /dev/null
@@ -1,22 +0,0 @@
-<?php
-
-namespace MediaWiki\Edit;
-
-use ParserOutput;
-use MediaWikiTestCase;
-
-/**
- * @covers \MediaWiki\Edit\PreparedEdit
- */
-class PreparedEditTest extends MediaWikiTestCase {
-       function testCallback() {
-               $output = new ParserOutput();
-               $edit = new PreparedEdit();
-               $edit->parserOutputCallback = function () {
-                       return new ParserOutput();
-               };
-
-               $this->assertEquals( $output, $edit->getOutput() );
-               $this->assertEquals( $output, $edit->output );
-       }
-}
diff --git a/tests/phpunit/includes/TitleArrayFromResultTest.php b/tests/phpunit/includes/TitleArrayFromResultTest.php
deleted file mode 100644 (file)
index 32c7571..0000000
+++ /dev/null
@@ -1,117 +0,0 @@
-<?php
-
-/**
- * @author Addshore
- * @covers TitleArrayFromResult
- */
-class TitleArrayFromResultTest extends PHPUnit\Framework\TestCase {
-
-       use MediaWikiCoversValidator;
-
-       private function getMockResultWrapper( $row = null, $numRows = 1 ) {
-               $resultWrapper = $this->getMockBuilder( Wikimedia\Rdbms\ResultWrapper::class )
-                       ->disableOriginalConstructor();
-
-               $resultWrapper = $resultWrapper->getMock();
-               $resultWrapper->expects( $this->atLeastOnce() )
-                       ->method( 'current' )
-                       ->will( $this->returnValue( $row ) );
-               $resultWrapper->expects( $this->any() )
-                       ->method( 'numRows' )
-                       ->will( $this->returnValue( $numRows ) );
-
-               return $resultWrapper;
-       }
-
-       private function getRowWithTitle( $namespace = 3, $title = 'foo' ) {
-               $row = new stdClass();
-               $row->page_namespace = $namespace;
-               $row->page_title = $title;
-               return $row;
-       }
-
-       /**
-        * @covers TitleArrayFromResult::__construct
-        */
-       public function testConstructionWithFalseRow() {
-               $row = false;
-               $resultWrapper = $this->getMockResultWrapper( $row );
-
-               $object = new TitleArrayFromResult( $resultWrapper );
-
-               $this->assertEquals( $resultWrapper, $object->res );
-               $this->assertSame( 0, $object->key );
-               $this->assertEquals( $row, $object->current );
-       }
-
-       /**
-        * @covers TitleArrayFromResult::__construct
-        */
-       public function testConstructionWithRow() {
-               $namespace = 0;
-               $title = 'foo';
-               $row = $this->getRowWithTitle( $namespace, $title );
-               $resultWrapper = $this->getMockResultWrapper( $row );
-
-               $object = new TitleArrayFromResult( $resultWrapper );
-
-               $this->assertEquals( $resultWrapper, $object->res );
-               $this->assertSame( 0, $object->key );
-               $this->assertInstanceOf( Title::class, $object->current );
-               $this->assertEquals( $namespace, $object->current->mNamespace );
-               $this->assertEquals( $title, $object->current->mTextform );
-       }
-
-       public static function provideNumberOfRows() {
-               return [
-                       [ 0 ],
-                       [ 1 ],
-                       [ 122 ],
-               ];
-       }
-
-       /**
-        * @dataProvider provideNumberOfRows
-        * @covers TitleArrayFromResult::count
-        */
-       public function testCountWithVaryingValues( $numRows ) {
-               $object = new TitleArrayFromResult( $this->getMockResultWrapper(
-                       $this->getRowWithTitle(),
-                       $numRows
-               ) );
-               $this->assertEquals( $numRows, $object->count() );
-       }
-
-       /**
-        * @covers TitleArrayFromResult::current
-        */
-       public function testCurrentAfterConstruction() {
-               $namespace = 0;
-               $title = 'foo';
-               $row = $this->getRowWithTitle( $namespace, $title );
-               $object = new TitleArrayFromResult( $this->getMockResultWrapper( $row ) );
-               $this->assertInstanceOf( Title::class, $object->current() );
-               $this->assertEquals( $namespace, $object->current->mNamespace );
-               $this->assertEquals( $title, $object->current->mTextform );
-       }
-
-       public function provideTestValid() {
-               return [
-                       [ $this->getRowWithTitle(), true ],
-                       [ false, false ],
-               ];
-       }
-
-       /**
-        * @dataProvider provideTestValid
-        * @covers TitleArrayFromResult::valid
-        */
-       public function testValid( $input, $expected ) {
-               $object = new TitleArrayFromResult( $this->getMockResultWrapper( $input ) );
-               $this->assertEquals( $expected, $object->valid() );
-       }
-
-       // @todo unit test for key()
-       // @todo unit test for next()
-       // @todo unit test for rewind()
-}
diff --git a/tests/phpunit/includes/WikiReferenceTest.php b/tests/phpunit/includes/WikiReferenceTest.php
deleted file mode 100644 (file)
index e4b21ce..0000000
+++ /dev/null
@@ -1,166 +0,0 @@
-<?php
-
-/**
- * @covers WikiReference
- */
-class WikiReferenceTest extends PHPUnit\Framework\TestCase {
-
-       use MediaWikiCoversValidator;
-
-       public function provideGetDisplayName() {
-               return [
-                       'http' => [ 'foo.bar', 'http://foo.bar' ],
-                       'https' => [ 'foo.bar', 'http://foo.bar' ],
-
-                       // apparently, this is the expected behavior
-                       'invalid' => [ 'purple kittens', 'purple kittens' ],
-               ];
-       }
-
-       /**
-        * @dataProvider provideGetDisplayName
-        */
-       public function testGetDisplayName( $expected, $canonicalServer ) {
-               $reference = new WikiReference( $canonicalServer, '/wiki/$1' );
-               $this->assertEquals( $expected, $reference->getDisplayName() );
-       }
-
-       public function testGetCanonicalServer() {
-               $reference = new WikiReference( 'https://acme.com', '/wiki/$1', '//acme.com' );
-               $this->assertEquals( 'https://acme.com', $reference->getCanonicalServer() );
-       }
-
-       public function provideGetCanonicalUrl() {
-               return [
-                       'no fragment' => [
-                               'https://acme.com/wiki/Foo',
-                               'https://acme.com',
-                               '//acme.com',
-                               '/wiki/$1',
-                               'Foo',
-                               null
-                       ],
-                       'empty fragment' => [
-                               'https://acme.com/wiki/Foo',
-                               'https://acme.com',
-                               '//acme.com',
-                               '/wiki/$1',
-                               'Foo',
-                               ''
-                       ],
-                       'fragment' => [
-                               'https://acme.com/wiki/Foo#Bar',
-                               'https://acme.com',
-                               '//acme.com',
-                               '/wiki/$1',
-                               'Foo',
-                               'Bar'
-                       ],
-                       'double fragment' => [
-                               'https://acme.com/wiki/Foo#Bar%23Xus',
-                               'https://acme.com',
-                               '//acme.com',
-                               '/wiki/$1',
-                               'Foo',
-                               'Bar#Xus'
-                       ],
-                       'escaped fragment' => [
-                               'https://acme.com/wiki/Foo%23Bar',
-                               'https://acme.com',
-                               '//acme.com',
-                               '/wiki/$1',
-                               'Foo#Bar',
-                               null
-                       ],
-                       'empty path' => [
-                               'https://acme.com/Foo',
-                               'https://acme.com',
-                               '//acme.com',
-                               '/$1',
-                               'Foo',
-                               null
-                       ],
-               ];
-       }
-
-       /**
-        * @dataProvider provideGetCanonicalUrl
-        */
-       public function testGetCanonicalUrl(
-               $expected, $canonicalServer, $server, $path, $page, $fragmentId
-       ) {
-               $reference = new WikiReference( $canonicalServer, $path, $server );
-               $this->assertEquals( $expected, $reference->getCanonicalUrl( $page, $fragmentId ) );
-       }
-
-       /**
-        * @dataProvider provideGetCanonicalUrl
-        * @note getUrl is an alias for getCanonicalUrl
-        */
-       public function testGetUrl( $expected, $canonicalServer, $server, $path, $page, $fragmentId ) {
-               $reference = new WikiReference( $canonicalServer, $path, $server );
-               $this->assertEquals( $expected, $reference->getUrl( $page, $fragmentId ) );
-       }
-
-       public function provideGetFullUrl() {
-               return [
-                       'no fragment' => [
-                               '//acme.com/wiki/Foo',
-                               'https://acme.com',
-                               '//acme.com',
-                               '/wiki/$1',
-                               'Foo',
-                               null
-                       ],
-                       'empty fragment' => [
-                               '//acme.com/wiki/Foo',
-                               'https://acme.com',
-                               '//acme.com',
-                               '/wiki/$1',
-                               'Foo',
-                               ''
-                       ],
-                       'fragment' => [
-                               '//acme.com/wiki/Foo#Bar',
-                               'https://acme.com',
-                               '//acme.com',
-                               '/wiki/$1',
-                               'Foo',
-                               'Bar'
-                       ],
-                       'double fragment' => [
-                               '//acme.com/wiki/Foo#Bar%23Xus',
-                               'https://acme.com',
-                               '//acme.com',
-                               '/wiki/$1',
-                               'Foo',
-                               'Bar#Xus'
-                       ],
-                       'escaped fragment' => [
-                               '//acme.com/wiki/Foo%23Bar',
-                               'https://acme.com',
-                               '//acme.com',
-                               '/wiki/$1',
-                               'Foo#Bar',
-                               null
-                       ],
-                       'empty path' => [
-                               '//acme.com/Foo',
-                               'https://acme.com',
-                               '//acme.com',
-                               '/$1',
-                               'Foo',
-                               null
-                       ],
-               ];
-       }
-
-       /**
-        * @dataProvider provideGetFullUrl
-        */
-       public function testGetFullUrl( $expected, $canonicalServer, $server, $path, $page, $fragmentId ) {
-               $reference = new WikiReference( $canonicalServer, $path, $server );
-               $this->assertEquals( $expected, $reference->getFullUrl( $page, $fragmentId ) );
-       }
-
-}
diff --git a/tests/phpunit/includes/XmlJsTest.php b/tests/phpunit/includes/XmlJsTest.php
deleted file mode 100644 (file)
index c7975ef..0000000
+++ /dev/null
@@ -1,26 +0,0 @@
-<?php
-
-/**
- * @group Xml
- */
-class XmlJsTest extends PHPUnit\Framework\TestCase {
-
-       use MediaWikiCoversValidator;
-
-       /**
-        * @covers XmlJsCode::__construct
-        * @dataProvider provideConstruction
-        */
-       public function testConstruction( $value ) {
-               $obj = new XmlJsCode( $value );
-               $this->assertEquals( $value, $obj->value );
-       }
-
-       public static function provideConstruction() {
-               return [
-                       [ null ],
-                       [ '' ],
-               ];
-       }
-
-}
diff --git a/tests/phpunit/includes/XmlSelectTest.php b/tests/phpunit/includes/XmlSelectTest.php
deleted file mode 100644 (file)
index 52e20bd..0000000
+++ /dev/null
@@ -1,182 +0,0 @@
-<?php
-
-/**
- * @group Xml
- */
-class XmlSelectTest extends MediaWikiTestCase {
-
-       /**
-        * @var XmlSelect
-        */
-       protected $select;
-
-       protected function setUp() {
-               parent::setUp();
-               $this->select = new XmlSelect();
-       }
-
-       protected function tearDown() {
-               parent::tearDown();
-               $this->select = null;
-       }
-
-       /**
-        * @covers XmlSelect::__construct
-        */
-       public function testConstructWithoutParameters() {
-               $this->assertEquals( '<select></select>', $this->select->getHTML() );
-       }
-
-       /**
-        * Parameters are $name (false), $id (false), $default (false)
-        * @dataProvider provideConstructionParameters
-        * @covers XmlSelect::__construct
-        */
-       public function testConstructParameters( $name, $id, $default, $expected ) {
-               $this->select = new XmlSelect( $name, $id, $default );
-               $this->assertEquals( $expected, $this->select->getHTML() );
-       }
-
-       /**
-        * Provide parameters for testConstructParameters() which use three
-        * parameters:
-        *  - $name    (default: false)
-        *  - $id      (default: false)
-        *  - $default (default: false)
-        * Provides a fourth parameters representing the expected HTML output
-        */
-       public static function provideConstructionParameters() {
-               return [
-                       /**
-                        * Values are set following a 3-bit Gray code where two successive
-                        * values differ by only one value.
-                        * See https://en.wikipedia.org/wiki/Gray_code
-                        */
-                       #      $name   $id    $default
-                       [ false, false, false, '<select></select>' ],
-                       [ false, false, 'foo', '<select></select>' ],
-                       [ false, 'id', 'foo', '<select id="id"></select>' ],
-                       [ false, 'id', false, '<select id="id"></select>' ],
-                       [ 'name', 'id', false, '<select name="name" id="id"></select>' ],
-                       [ 'name', 'id', 'foo', '<select name="name" id="id"></select>' ],
-                       [ 'name', false, 'foo', '<select name="name"></select>' ],
-                       [ 'name', false, false, '<select name="name"></select>' ],
-               ];
-       }
-
-       /**
-        * @covers XmlSelect::addOption
-        */
-       public function testAddOption() {
-               $this->select->addOption( 'foo' );
-               $this->assertEquals(
-                       '<select><option value="foo">foo</option></select>',
-                       $this->select->getHTML()
-               );
-       }
-
-       /**
-        * @covers XmlSelect::addOption
-        */
-       public function testAddOptionWithDefault() {
-               $this->select->addOption( 'foo', true );
-               $this->assertEquals(
-                       '<select><option value="1">foo</option></select>',
-                       $this->select->getHTML()
-               );
-       }
-
-       /**
-        * @covers XmlSelect::addOption
-        */
-       public function testAddOptionWithFalse() {
-               $this->select->addOption( 'foo', false );
-               $this->assertEquals(
-                       '<select><option value="foo">foo</option></select>',
-                       $this->select->getHTML()
-               );
-       }
-
-       /**
-        * @covers XmlSelect::addOption
-        */
-       public function testAddOptionWithValueZero() {
-               $this->select->addOption( 'foo', 0 );
-               $this->assertEquals(
-                       '<select><option value="0">foo</option></select>',
-                       $this->select->getHTML()
-               );
-       }
-
-       /**
-        * @covers XmlSelect::setDefault
-        */
-       public function testSetDefault() {
-               $this->select->setDefault( 'bar1' );
-               $this->select->addOption( 'foo1' );
-               $this->select->addOption( 'bar1' );
-               $this->select->addOption( 'foo2' );
-               $this->assertEquals(
-                       '<select><option value="foo1">foo1</option>' . "\n" .
-                               '<option value="bar1" selected="">bar1</option>' . "\n" .
-                               '<option value="foo2">foo2</option></select>', $this->select->getHTML() );
-       }
-
-       /**
-        * Adding default later on should set the correct selection or
-        * raise an exception.
-        * To handle this, we need to render the options in getHtml()
-        * @covers XmlSelect::setDefault
-        */
-       public function testSetDefaultAfterAddingOptions() {
-               $this->select->addOption( 'foo1' );
-               $this->select->addOption( 'bar1' );
-               $this->select->addOption( 'foo2' );
-               $this->select->setDefault( 'bar1' ); # setting default after adding options
-               $this->assertEquals(
-                       '<select><option value="foo1">foo1</option>' . "\n" .
-                               '<option value="bar1" selected="">bar1</option>' . "\n" .
-                               '<option value="foo2">foo2</option></select>', $this->select->getHTML() );
-       }
-
-       /**
-        * @covers XmlSelect::setAttribute
-        * @covers XmlSelect::getAttribute
-        */
-       public function testGetAttributes() {
-               # create some attributes
-               $this->select->setAttribute( 'dummy', 0x777 );
-               $this->select->setAttribute( 'string', 'euro €' );
-               $this->select->setAttribute( 1911, 'razor' );
-
-               # verify we can retrieve them
-               $this->assertEquals(
-                       $this->select->getAttribute( 'dummy' ),
-                       0x777
-               );
-               $this->assertEquals(
-                       $this->select->getAttribute( 'string' ),
-                       'euro €'
-               );
-               $this->assertEquals(
-                       $this->select->getAttribute( 1911 ),
-                       'razor'
-               );
-
-               # inexistent keys should give us 'null'
-               $this->assertEquals(
-                       $this->select->getAttribute( 'I DO NOT EXIT' ),
-                       null
-               );
-
-               # verify string / integer
-               $this->assertEquals(
-                       $this->select->getAttribute( '1911' ),
-                       'razor'
-               );
-               $this->assertEquals(
-                       $this->select->getAttribute( 'dummy' ),
-                       0x777
-               );
-       }
-}
diff --git a/tests/phpunit/includes/actions/ViewActionTest.php b/tests/phpunit/includes/actions/ViewActionTest.php
deleted file mode 100644 (file)
index 5f659c0..0000000
+++ /dev/null
@@ -1,35 +0,0 @@
-<?php
-
-/**
- * @covers \ViewAction
- *
- * @group Actions
- *
- * @author Derick N. Alangi
- */
-class ViewActionTest extends MediaWikiTestCase {
-       /**
-        * @return ViewAction
-        */
-       private function makeViewActionClassFactory() {
-               $page = new Article( Title::newMainPage() );
-               $context = RequestContext::getMain();
-               $viewAction = new ViewAction( $page, $context );
-
-               return $viewAction;
-       }
-
-       public function testGetName() {
-               $viewAction = $this->makeViewActionClassFactory();
-               $actual = $viewAction->getName();
-
-               $this->assertSame( 'view', $actual );
-       }
-
-       public function testOnView() {
-               $viewAction = $this->makeViewActionClassFactory();
-               $actual = $viewAction->onView();
-
-               $this->assertNull( $actual );
-       }
-}
diff --git a/tests/phpunit/includes/api/ApiBlockInfoTraitTest.php b/tests/phpunit/includes/api/ApiBlockInfoTraitTest.php
deleted file mode 100644 (file)
index ba5c003..0000000
+++ /dev/null
@@ -1,43 +0,0 @@
-<?php
-
-use Wikimedia\TestingAccessWrapper;
-use MediaWiki\Block\DatabaseBlock;
-use MediaWiki\Block\SystemBlock;
-
-/**
- * @covers ApiBlockInfoTrait
- */
-class ApiBlockInfoTraitTest extends MediaWikiTestCase {
-       /**
-        * @dataProvider provideGetBlockDetails
-        */
-       public function testGetBlockDetails( $block, $expectedInfo ) {
-               $mock = $this->getMockForTrait( ApiBlockInfoTrait::class );
-               $info = TestingAccessWrapper::newFromObject( $mock )->getBlockDetails( $block );
-               $subset = array_merge( [
-                       'blockid' => null,
-                       'blockedby' => '',
-                       'blockedbyid' => 0,
-                       'blockreason' => '',
-                       'blockexpiry' => 'infinite',
-               ], $expectedInfo );
-               $this->assertArraySubset( $subset, $info );
-       }
-
-       public static function provideGetBlockDetails() {
-               return [
-                       'Sitewide block' => [
-                               new DatabaseBlock(),
-                               [ 'blockpartial' => false ],
-                       ],
-                       'Partial block' => [
-                               new DatabaseBlock( [ 'sitewide' => false ] ),
-                               [ 'blockpartial' => true ],
-                       ],
-                       'System block' => [
-                               new SystemBlock( [ 'systemBlock' => 'proxy' ] ),
-                               [ 'systemblocktype' => 'proxy' ]
-                       ],
-               ];
-       }
-}
diff --git a/tests/phpunit/includes/api/ApiContinuationManagerTest.php b/tests/phpunit/includes/api/ApiContinuationManagerTest.php
deleted file mode 100644 (file)
index 788d120..0000000
+++ /dev/null
@@ -1,198 +0,0 @@
-<?php
-
-/**
- * @covers ApiContinuationManager
- * @group API
- */
-class ApiContinuationManagerTest extends MediaWikiTestCase {
-
-       private static function getManager( $continue, $allModules, $generatedModules ) {
-               $context = new DerivativeContext( RequestContext::getMain() );
-               $context->setRequest( new FauxRequest( [ 'continue' => $continue ] ) );
-               $main = new ApiMain( $context );
-               return new ApiContinuationManager( $main, $allModules, $generatedModules );
-       }
-
-       public function testContinuation() {
-               $allModules = [
-                       new MockApiQueryBase( 'mock1' ),
-                       new MockApiQueryBase( 'mock2' ),
-                       new MockApiQueryBase( 'mocklist' ),
-               ];
-               $generator = new MockApiQueryBase( 'generator' );
-
-               $manager = self::getManager( '', $allModules, [ 'mock1', 'mock2' ] );
-               $this->assertSame( ApiMain::class, $manager->getSource() );
-               $this->assertSame( false, $manager->isGeneratorDone() );
-               $this->assertSame( $allModules, $manager->getRunModules() );
-               $manager->addContinueParam( $allModules[0], 'm1continue', [ 1, 2 ] );
-               $manager->addContinueParam( $allModules[2], 'mlcontinue', 2 );
-               $manager->addGeneratorContinueParam( $generator, 'gcontinue', 3 );
-               $this->assertSame( [ [
-                       'mlcontinue' => 2,
-                       'm1continue' => '1|2',
-                       'continue' => '||mock2',
-               ], false ], $manager->getContinuation() );
-               $this->assertSame( [
-                       'mock1' => [ 'm1continue' => '1|2' ],
-                       'mocklist' => [ 'mlcontinue' => 2 ],
-                       'generator' => [ 'gcontinue' => 3 ],
-               ], $manager->getRawContinuation() );
-
-               $result = new ApiResult( 0 );
-               $manager->setContinuationIntoResult( $result );
-               $this->assertSame( [
-                       'mlcontinue' => 2,
-                       'm1continue' => '1|2',
-                       'continue' => '||mock2',
-               ], $result->getResultData( 'continue' ) );
-               $this->assertSame( null, $result->getResultData( 'batchcomplete' ) );
-
-               $manager = self::getManager( '', $allModules, [ 'mock1', 'mock2' ] );
-               $this->assertSame( false, $manager->isGeneratorDone() );
-               $this->assertSame( $allModules, $manager->getRunModules() );
-               $manager->addContinueParam( $allModules[0], 'm1continue', [ 1, 2 ] );
-               $manager->addGeneratorContinueParam( $generator, 'gcontinue', [ 3, 4 ] );
-               $this->assertSame( [ [
-                       'm1continue' => '1|2',
-                       'continue' => '||mock2|mocklist',
-               ], false ], $manager->getContinuation() );
-               $this->assertSame( [
-                       'mock1' => [ 'm1continue' => '1|2' ],
-                       'generator' => [ 'gcontinue' => '3|4' ],
-               ], $manager->getRawContinuation() );
-
-               $manager = self::getManager( '', $allModules, [ 'mock1', 'mock2' ] );
-               $this->assertSame( false, $manager->isGeneratorDone() );
-               $this->assertSame( $allModules, $manager->getRunModules() );
-               $manager->addContinueParam( $allModules[2], 'mlcontinue', 2 );
-               $manager->addGeneratorContinueParam( $generator, 'gcontinue', 3 );
-               $this->assertSame( [ [
-                       'mlcontinue' => 2,
-                       'gcontinue' => 3,
-                       'continue' => 'gcontinue||',
-               ], true ], $manager->getContinuation() );
-               $this->assertSame( [
-                       'mocklist' => [ 'mlcontinue' => 2 ],
-                       'generator' => [ 'gcontinue' => 3 ],
-               ], $manager->getRawContinuation() );
-
-               $result = new ApiResult( 0 );
-               $manager->setContinuationIntoResult( $result );
-               $this->assertSame( [
-                       'mlcontinue' => 2,
-                       'gcontinue' => 3,
-                       'continue' => 'gcontinue||',
-               ], $result->getResultData( 'continue' ) );
-               $this->assertSame( true, $result->getResultData( 'batchcomplete' ) );
-
-               $manager = self::getManager( '', $allModules, [ 'mock1', 'mock2' ] );
-               $this->assertSame( false, $manager->isGeneratorDone() );
-               $this->assertSame( $allModules, $manager->getRunModules() );
-               $manager->addGeneratorContinueParam( $generator, 'gcontinue', 3 );
-               $this->assertSame( [ [
-                       'gcontinue' => 3,
-                       'continue' => 'gcontinue||mocklist',
-               ], true ], $manager->getContinuation() );
-               $this->assertSame( [
-                       'generator' => [ 'gcontinue' => 3 ],
-               ], $manager->getRawContinuation() );
-
-               $manager = self::getManager( '', $allModules, [ 'mock1', 'mock2' ] );
-               $this->assertSame( false, $manager->isGeneratorDone() );
-               $this->assertSame( $allModules, $manager->getRunModules() );
-               $manager->addContinueParam( $allModules[0], 'm1continue', [ 1, 2 ] );
-               $manager->addContinueParam( $allModules[2], 'mlcontinue', 2 );
-               $this->assertSame( [ [
-                       'mlcontinue' => 2,
-                       'm1continue' => '1|2',
-                       'continue' => '||mock2',
-               ], false ], $manager->getContinuation() );
-               $this->assertSame( [
-                       'mock1' => [ 'm1continue' => '1|2' ],
-                       'mocklist' => [ 'mlcontinue' => 2 ],
-               ], $manager->getRawContinuation() );
-
-               $manager = self::getManager( '', $allModules, [ 'mock1', 'mock2' ] );
-               $this->assertSame( false, $manager->isGeneratorDone() );
-               $this->assertSame( $allModules, $manager->getRunModules() );
-               $manager->addContinueParam( $allModules[0], 'm1continue', [ 1, 2 ] );
-               $this->assertSame( [ [
-                       'm1continue' => '1|2',
-                       'continue' => '||mock2|mocklist',
-               ], false ], $manager->getContinuation() );
-               $this->assertSame( [
-                       'mock1' => [ 'm1continue' => '1|2' ],
-               ], $manager->getRawContinuation() );
-
-               $manager = self::getManager( '', $allModules, [ 'mock1', 'mock2' ] );
-               $this->assertSame( false, $manager->isGeneratorDone() );
-               $this->assertSame( $allModules, $manager->getRunModules() );
-               $manager->addContinueParam( $allModules[2], 'mlcontinue', 2 );
-               $this->assertSame( [ [
-                       'mlcontinue' => 2,
-                       'continue' => '-||mock1|mock2',
-               ], true ], $manager->getContinuation() );
-               $this->assertSame( [
-                       'mocklist' => [ 'mlcontinue' => 2 ],
-               ], $manager->getRawContinuation() );
-
-               $manager = self::getManager( '', $allModules, [ 'mock1', 'mock2' ] );
-               $this->assertSame( false, $manager->isGeneratorDone() );
-               $this->assertSame( $allModules, $manager->getRunModules() );
-               $this->assertSame( [ [], true ], $manager->getContinuation() );
-               $this->assertSame( [], $manager->getRawContinuation() );
-
-               $manager = self::getManager( '||mock2', $allModules, [ 'mock1', 'mock2' ] );
-               $this->assertSame( false, $manager->isGeneratorDone() );
-               $this->assertSame(
-                       array_values( array_diff_key( $allModules, [ 1 => 1 ] ) ),
-                       $manager->getRunModules()
-               );
-
-               $manager = self::getManager( '-||', $allModules, [ 'mock1', 'mock2' ] );
-               $this->assertSame( true, $manager->isGeneratorDone() );
-               $this->assertSame(
-                       array_values( array_diff_key( $allModules, [ 0 => 0, 1 => 1 ] ) ),
-                       $manager->getRunModules()
-               );
-
-               try {
-                       self::getManager( 'foo', $allModules, [ 'mock1', 'mock2' ] );
-                       $this->fail( 'Expected exception not thrown' );
-               } catch ( ApiUsageException $ex ) {
-                       $this->assertTrue( ApiTestCase::apiExceptionHasCode( $ex, 'badcontinue' ),
-                               'Expected exception'
-                       );
-               }
-
-               $manager = self::getManager(
-                       '||mock2',
-                       array_slice( $allModules, 0, 2 ),
-                       [ 'mock1', 'mock2' ]
-               );
-               try {
-                       $manager->addContinueParam( $allModules[1], 'm2continue', 1 );
-                       $this->fail( 'Expected exception not thrown' );
-               } catch ( UnexpectedValueException $ex ) {
-                       $this->assertSame(
-                               'Module \'mock2\' was not supposed to have been executed, ' .
-                                       'but it was executed anyway',
-                               $ex->getMessage(),
-                               'Expected exception'
-                       );
-               }
-               try {
-                       $manager->addContinueParam( $allModules[2], 'mlcontinue', 1 );
-                       $this->fail( 'Expected exception not thrown' );
-               } catch ( UnexpectedValueException $ex ) {
-                       $this->assertSame(
-                               'Module \'mocklist\' called ApiContinuationManager::addContinueParam ' .
-                                       'but was not passed to ApiContinuationManager::__construct',
-                               $ex->getMessage(),
-                               'Expected exception'
-                       );
-               }
-       }
-
-}
diff --git a/tests/phpunit/includes/api/ApiMessageTest.php b/tests/phpunit/includes/api/ApiMessageTest.php
deleted file mode 100644 (file)
index 70114c2..0000000
+++ /dev/null
@@ -1,196 +0,0 @@
-<?php
-
-use Wikimedia\TestingAccessWrapper;
-
-/**
- * @group API
- */
-class ApiMessageTest extends MediaWikiTestCase {
-
-       private function compareMessages( Message $msg, Message $msg2 ) {
-               $this->assertSame( $msg->getKey(), $msg2->getKey(), 'getKey' );
-               $this->assertSame( $msg->getKeysToTry(), $msg2->getKeysToTry(), 'getKeysToTry' );
-               $this->assertSame( $msg->getParams(), $msg2->getParams(), 'getParams' );
-               $this->assertSame( $msg->getLanguage(), $msg2->getLanguage(), 'getLanguage' );
-
-               $msg = TestingAccessWrapper::newFromObject( $msg );
-               $msg2 = TestingAccessWrapper::newFromObject( $msg2 );
-               $this->assertSame( $msg->interface, $msg2->interface, 'interface' );
-               $this->assertSame( $msg->useDatabase, $msg2->useDatabase, 'useDatabase' );
-               $this->assertSame( $msg->format, $msg2->format, 'format' );
-               $this->assertSame(
-                       $msg->title ? $msg->title->getFullText() : null,
-                       $msg2->title ? $msg2->title->getFullText() : null,
-                       'title'
-               );
-       }
-
-       /**
-        * @covers ApiMessageTrait
-        */
-       public function testCodeDefaults() {
-               $msg = new ApiMessage( 'foo' );
-               $this->assertSame( 'foo', $msg->getApiCode() );
-
-               $msg = new ApiMessage( 'apierror-bar' );
-               $this->assertSame( 'bar', $msg->getApiCode() );
-
-               $msg = new ApiMessage( 'apiwarn-baz' );
-               $this->assertSame( 'baz', $msg->getApiCode() );
-
-               // Weird "message key"
-               $msg = new ApiMessage( "<foo> bar\nbaz" );
-               $this->assertSame( '_foo__bar_baz', $msg->getApiCode() );
-
-               // BC case
-               $msg = new ApiMessage( 'actionthrottledtext' );
-               $this->assertSame( 'ratelimited', $msg->getApiCode() );
-
-               $msg = new ApiMessage( [ 'apierror-missingparam', 'param' ] );
-               $this->assertSame( 'noparam', $msg->getApiCode() );
-       }
-
-       /**
-        * @covers ApiMessageTrait
-        * @dataProvider provideInvalidCode
-        * @param mixed $code
-        */
-       public function testInvalidCode( $code ) {
-               $msg = new ApiMessage( 'foo' );
-               try {
-                       $msg->setApiCode( $code );
-                       $this->fail( 'Expected exception not thrown' );
-               } catch ( InvalidArgumentException $ex ) {
-                       $this->assertTrue( true );
-               }
-
-               try {
-                       new ApiMessage( 'foo', $code );
-                       $this->fail( 'Expected exception not thrown' );
-               } catch ( InvalidArgumentException $ex ) {
-                       $this->assertTrue( true );
-               }
-       }
-
-       public static function provideInvalidCode() {
-               return [
-                       [ '' ],
-                       [ 42 ],
-                       [ 'A bad code' ],
-                       [ 'Project:A_page_title' ],
-                       [ "WTF\nnewlines" ],
-               ];
-       }
-
-       /**
-        * @covers ApiMessage
-        * @covers ApiMessageTrait
-        */
-       public function testApiMessage() {
-               $msg = new Message( [ 'foo', 'bar' ], [ 'baz' ] );
-               $msg->inLanguage( 'de' )->title( Title::newMainPage() );
-               $msg2 = new ApiMessage( $msg, 'code', [ 'data' ] );
-               $this->compareMessages( $msg, $msg2 );
-               $this->assertEquals( 'code', $msg2->getApiCode() );
-               $this->assertEquals( [ 'data' ], $msg2->getApiData() );
-
-               $msg2 = unserialize( serialize( $msg2 ) );
-               $this->compareMessages( $msg, $msg2 );
-               $this->assertEquals( 'code', $msg2->getApiCode() );
-               $this->assertEquals( [ 'data' ], $msg2->getApiData() );
-
-               $msg = new Message( [ 'foo', 'bar' ], [ 'baz' ] );
-               $msg2 = new ApiMessage( [ [ 'foo', 'bar' ], 'baz' ], 'code', [ 'data' ] );
-               $this->compareMessages( $msg, $msg2 );
-               $this->assertEquals( 'code', $msg2->getApiCode() );
-               $this->assertEquals( [ 'data' ], $msg2->getApiData() );
-
-               $msg = new Message( 'foo' );
-               $msg2 = new ApiMessage( 'foo' );
-               $this->compareMessages( $msg, $msg2 );
-               $this->assertEquals( 'foo', $msg2->getApiCode() );
-               $this->assertEquals( [], $msg2->getApiData() );
-
-               $msg2->setApiCode( 'code', [ 'data' ] );
-               $this->assertEquals( 'code', $msg2->getApiCode() );
-               $this->assertEquals( [ 'data' ], $msg2->getApiData() );
-               $msg2->setApiCode( null );
-               $this->assertEquals( 'foo', $msg2->getApiCode() );
-               $this->assertEquals( [ 'data' ], $msg2->getApiData() );
-               $msg2->setApiData( [ 'data2' ] );
-               $this->assertEquals( [ 'data2' ], $msg2->getApiData() );
-       }
-
-       /**
-        * @covers ApiRawMessage
-        * @covers ApiMessageTrait
-        */
-       public function testApiRawMessage() {
-               $msg = new RawMessage( 'foo', [ 'baz' ] );
-               $msg->inLanguage( 'de' )->title( Title::newMainPage() );
-               $msg2 = new ApiRawMessage( $msg, 'code', [ 'data' ] );
-               $this->compareMessages( $msg, $msg2 );
-               $this->assertEquals( 'code', $msg2->getApiCode() );
-               $this->assertEquals( [ 'data' ], $msg2->getApiData() );
-
-               $msg2 = unserialize( serialize( $msg2 ) );
-               $this->compareMessages( $msg, $msg2 );
-               $this->assertEquals( 'code', $msg2->getApiCode() );
-               $this->assertEquals( [ 'data' ], $msg2->getApiData() );
-
-               $msg = new RawMessage( 'foo', [ 'baz' ] );
-               $msg2 = new ApiRawMessage( [ 'foo', 'baz' ], 'code', [ 'data' ] );
-               $this->compareMessages( $msg, $msg2 );
-               $this->assertEquals( 'code', $msg2->getApiCode() );
-               $this->assertEquals( [ 'data' ], $msg2->getApiData() );
-
-               $msg = new RawMessage( 'foo' );
-               $msg2 = new ApiRawMessage( 'foo', 'code', [ 'data' ] );
-               $this->compareMessages( $msg, $msg2 );
-               $this->assertEquals( 'code', $msg2->getApiCode() );
-               $this->assertEquals( [ 'data' ], $msg2->getApiData() );
-
-               $msg2->setApiCode( 'code', [ 'data' ] );
-               $this->assertEquals( 'code', $msg2->getApiCode() );
-               $this->assertEquals( [ 'data' ], $msg2->getApiData() );
-               $msg2->setApiCode( null );
-               $this->assertEquals( 'foo', $msg2->getApiCode() );
-               $this->assertEquals( [ 'data' ], $msg2->getApiData() );
-               $msg2->setApiData( [ 'data2' ] );
-               $this->assertEquals( [ 'data2' ], $msg2->getApiData() );
-       }
-
-       /**
-        * @covers ApiMessage::create
-        */
-       public function testApiMessageCreate() {
-               $this->assertInstanceOf( ApiMessage::class, ApiMessage::create( new Message( 'mainpage' ) ) );
-               $this->assertInstanceOf(
-                       ApiRawMessage::class, ApiMessage::create( new RawMessage( 'mainpage' ) )
-               );
-               $this->assertInstanceOf( ApiMessage::class, ApiMessage::create( 'mainpage' ) );
-
-               $msg = new ApiMessage( [ 'parentheses', 'foobar' ] );
-               $msg2 = new Message( 'parentheses', [ 'foobar' ] );
-
-               $this->assertSame( $msg, ApiMessage::create( $msg ) );
-               $this->assertEquals( $msg, ApiMessage::create( $msg2 ) );
-               $this->assertEquals( $msg, ApiMessage::create( [ 'parentheses', 'foobar' ] ) );
-               $this->assertEquals( $msg,
-                       ApiMessage::create( [ 'message' => 'parentheses', 'params' => [ 'foobar' ] ] )
-               );
-               $this->assertSame( $msg,
-                       ApiMessage::create( [ 'message' => $msg, 'params' => [ 'xxx' ] ] )
-               );
-               $this->assertEquals( $msg,
-                       ApiMessage::create( [ 'message' => $msg2, 'params' => [ 'xxx' ] ] )
-               );
-               $this->assertSame( $msg,
-                       ApiMessage::create( [ 'message' => $msg ] )
-               );
-
-               $msg = new ApiRawMessage( [ 'parentheses', 'foobar' ] );
-               $this->assertSame( $msg, ApiMessage::create( $msg ) );
-       }
-
-}
diff --git a/tests/phpunit/includes/api/ApiResultTest.php b/tests/phpunit/includes/api/ApiResultTest.php
deleted file mode 100644 (file)
index 98e24fb..0000000
+++ /dev/null
@@ -1,1410 +0,0 @@
-<?php
-
-/**
- * @covers ApiResult
- * @group API
- */
-class ApiResultTest extends MediaWikiTestCase {
-
-       /**
-        * @covers ApiResult
-        */
-       public function testStaticDataMethods() {
-               $arr = [];
-
-               ApiResult::setValue( $arr, 'setValue', '1' );
-
-               ApiResult::setValue( $arr, null, 'unnamed 1' );
-               ApiResult::setValue( $arr, null, 'unnamed 2' );
-
-               ApiResult::setValue( $arr, 'deleteValue', '2' );
-               ApiResult::unsetValue( $arr, 'deleteValue' );
-
-               ApiResult::setContentValue( $arr, 'setContentValue', '3' );
-
-               $this->assertSame( [
-                       'setValue' => '1',
-                       'unnamed 1',
-                       'unnamed 2',
-                       ApiResult::META_CONTENT => 'setContentValue',
-                       'setContentValue' => '3',
-               ], $arr );
-
-               try {
-                       ApiResult::setValue( $arr, 'setValue', '99' );
-                       $this->fail( 'Expected exception not thrown' );
-               } catch ( RuntimeException $ex ) {
-                       $this->assertSame(
-                               'Attempting to add element setValue=99, existing value is 1',
-                               $ex->getMessage(),
-                               'Expected exception'
-                       );
-               }
-
-               try {
-                       ApiResult::setContentValue( $arr, 'setContentValue2', '99' );
-                       $this->fail( 'Expected exception not thrown' );
-               } catch ( RuntimeException $ex ) {
-                       $this->assertSame(
-                               'Attempting to set content element as setContentValue2 when setContentValue ' .
-                                       'is already set as the content element',
-                               $ex->getMessage(),
-                               'Expected exception'
-                       );
-               }
-
-               ApiResult::setValue( $arr, 'setValue', '99', ApiResult::OVERRIDE );
-               $this->assertSame( '99', $arr['setValue'] );
-
-               ApiResult::setContentValue( $arr, 'setContentValue2', '99', ApiResult::OVERRIDE );
-               $this->assertSame( 'setContentValue2', $arr[ApiResult::META_CONTENT] );
-
-               $arr = [ 'foo' => 1, 'bar' => 1 ];
-               ApiResult::setValue( $arr, 'top', '2', ApiResult::ADD_ON_TOP );
-               ApiResult::setValue( $arr, null, '2', ApiResult::ADD_ON_TOP );
-               ApiResult::setValue( $arr, 'bottom', '2' );
-               ApiResult::setValue( $arr, 'foo', '2', ApiResult::OVERRIDE );
-               ApiResult::setValue( $arr, 'bar', '2', ApiResult::OVERRIDE | ApiResult::ADD_ON_TOP );
-               $this->assertSame( [ 0, 'top', 'foo', 'bar', 'bottom' ], array_keys( $arr ) );
-
-               $arr = [];
-               ApiResult::setValue( $arr, 'sub', [ 'foo' => 1 ] );
-               ApiResult::setValue( $arr, 'sub', [ 'bar' => 1 ] );
-               $this->assertSame( [ 'sub' => [ 'foo' => 1, 'bar' => 1 ] ], $arr );
-
-               try {
-                       ApiResult::setValue( $arr, 'sub', [ 'foo' => 2, 'baz' => 2 ] );
-                       $this->fail( 'Expected exception not thrown' );
-               } catch ( RuntimeException $ex ) {
-                       $this->assertSame(
-                               'Conflicting keys (foo) when attempting to merge element sub',
-                               $ex->getMessage(),
-                               'Expected exception'
-                       );
-               }
-
-               $arr = [];
-               $title = Title::newFromText( "MediaWiki:Foobar" );
-               $obj = new stdClass;
-               $obj->foo = 1;
-               $obj->bar = 2;
-               ApiResult::setValue( $arr, 'title', $title );
-               ApiResult::setValue( $arr, 'obj', $obj );
-               $this->assertSame( [
-                       'title' => (string)$title,
-                       'obj' => [ 'foo' => 1, 'bar' => 2, ApiResult::META_TYPE => 'assoc' ],
-               ], $arr );
-
-               $fh = tmpfile();
-               try {
-                       ApiResult::setValue( $arr, 'file', $fh );
-                       $this->fail( 'Expected exception not thrown' );
-               } catch ( InvalidArgumentException $ex ) {
-                       $this->assertSame(
-                               'Cannot add resource(stream) to ApiResult',
-                               $ex->getMessage(),
-                               'Expected exception'
-                       );
-               }
-               try {
-                       ApiResult::setValue( $arr, null, $fh );
-                       $this->fail( 'Expected exception not thrown' );
-               } catch ( InvalidArgumentException $ex ) {
-                       $this->assertSame(
-                               'Cannot add resource(stream) to ApiResult',
-                               $ex->getMessage(),
-                               'Expected exception'
-                       );
-               }
-               try {
-                       $obj->file = $fh;
-                       ApiResult::setValue( $arr, 'sub', $obj );
-                       $this->fail( 'Expected exception not thrown' );
-               } catch ( InvalidArgumentException $ex ) {
-                       $this->assertSame(
-                               'Cannot add resource(stream) to ApiResult',
-                               $ex->getMessage(),
-                               'Expected exception'
-                       );
-               }
-               try {
-                       $obj->file = $fh;
-                       ApiResult::setValue( $arr, null, $obj );
-                       $this->fail( 'Expected exception not thrown' );
-               } catch ( InvalidArgumentException $ex ) {
-                       $this->assertSame(
-                               'Cannot add resource(stream) to ApiResult',
-                               $ex->getMessage(),
-                               'Expected exception'
-                       );
-               }
-               fclose( $fh );
-
-               try {
-                       ApiResult::setValue( $arr, 'inf', INF );
-                       $this->fail( 'Expected exception not thrown' );
-               } catch ( InvalidArgumentException $ex ) {
-                       $this->assertSame(
-                               'Cannot add non-finite floats to ApiResult',
-                               $ex->getMessage(),
-                               'Expected exception'
-                       );
-               }
-               try {
-                       ApiResult::setValue( $arr, null, INF );
-                       $this->fail( 'Expected exception not thrown' );
-               } catch ( InvalidArgumentException $ex ) {
-                       $this->assertSame(
-                               'Cannot add non-finite floats to ApiResult',
-                               $ex->getMessage(),
-                               'Expected exception'
-                       );
-               }
-               try {
-                       ApiResult::setValue( $arr, 'nan', NAN );
-                       $this->fail( 'Expected exception not thrown' );
-               } catch ( InvalidArgumentException $ex ) {
-                       $this->assertSame(
-                               'Cannot add non-finite floats to ApiResult',
-                               $ex->getMessage(),
-                               'Expected exception'
-                       );
-               }
-               try {
-                       ApiResult::setValue( $arr, null, NAN );
-                       $this->fail( 'Expected exception not thrown' );
-               } catch ( InvalidArgumentException $ex ) {
-                       $this->assertSame(
-                               'Cannot add non-finite floats to ApiResult',
-                               $ex->getMessage(),
-                               'Expected exception'
-                       );
-               }
-
-               ApiResult::setValue( $arr, null, NAN, ApiResult::NO_VALIDATE );
-
-               try {
-                       ApiResult::setValue( $arr, null, NAN, ApiResult::NO_SIZE_CHECK );
-                       $this->fail( 'Expected exception not thrown' );
-               } catch ( InvalidArgumentException $ex ) {
-                       $this->assertSame(
-                               'Cannot add non-finite floats to ApiResult',
-                               $ex->getMessage(),
-                               'Expected exception'
-                       );
-               }
-
-               $arr = [];
-               $result2 = new ApiResult( 8388608 );
-               $result2->addValue( null, 'foo', 'bar' );
-               ApiResult::setValue( $arr, 'baz', $result2 );
-               $this->assertSame( [
-                       'baz' => [
-                               ApiResult::META_TYPE => 'assoc',
-                               'foo' => 'bar',
-                       ]
-               ], $arr );
-
-               $arr = [];
-               ApiResult::setValue( $arr, 'foo', "foo\x80bar" );
-               ApiResult::setValue( $arr, 'bar', "a\xcc\x81" );
-               ApiResult::setValue( $arr, 'baz', 74 );
-               ApiResult::setValue( $arr, null, "foo\x80bar" );
-               ApiResult::setValue( $arr, null, "a\xcc\x81" );
-               $this->assertSame( [
-                       'foo' => "foo\xef\xbf\xbdbar",
-                       'bar' => "\xc3\xa1",
-                       'baz' => 74,
-                       0 => "foo\xef\xbf\xbdbar",
-                       1 => "\xc3\xa1",
-               ], $arr );
-
-               $obj = new stdClass;
-               $obj->{'1'} = 'one';
-               $arr = [];
-               ApiResult::setValue( $arr, 'foo', $obj );
-               $this->assertSame( [
-                       'foo' => [
-                               1 => 'one',
-                               ApiResult::META_TYPE => 'assoc',
-                       ]
-               ], $arr );
-       }
-
-       /**
-        * @covers ApiResult
-        */
-       public function testInstanceDataMethods() {
-               $result = new ApiResult( 8388608 );
-
-               $result->addValue( null, 'setValue', '1' );
-
-               $result->addValue( null, null, 'unnamed 1' );
-               $result->addValue( null, null, 'unnamed 2' );
-
-               $result->addValue( null, 'deleteValue', '2' );
-               $result->removeValue( null, 'deleteValue' );
-
-               $result->addValue( [ 'a', 'b' ], 'deleteValue', '3' );
-               $result->removeValue( [ 'a', 'b', 'deleteValue' ], null, '3' );
-
-               $result->addContentValue( null, 'setContentValue', '3' );
-
-               $this->assertSame( [
-                       'setValue' => '1',
-                       'unnamed 1',
-                       'unnamed 2',
-                       'a' => [ 'b' => [] ],
-                       'setContentValue' => '3',
-                       ApiResult::META_TYPE => 'assoc',
-                       ApiResult::META_CONTENT => 'setContentValue',
-               ], $result->getResultData() );
-               $this->assertSame( 20, $result->getSize() );
-
-               try {
-                       $result->addValue( null, 'setValue', '99' );
-                       $this->fail( 'Expected exception not thrown' );
-               } catch ( RuntimeException $ex ) {
-                       $this->assertSame(
-                               'Attempting to add element setValue=99, existing value is 1',
-                               $ex->getMessage(),
-                               'Expected exception'
-                       );
-               }
-
-               try {
-                       $result->addContentValue( null, 'setContentValue2', '99' );
-                       $this->fail( 'Expected exception not thrown' );
-               } catch ( RuntimeException $ex ) {
-                       $this->assertSame(
-                               'Attempting to set content element as setContentValue2 when setContentValue ' .
-                                       'is already set as the content element',
-                               $ex->getMessage(),
-                               'Expected exception'
-                       );
-               }
-
-               $result->addValue( null, 'setValue', '99', ApiResult::OVERRIDE );
-               $this->assertSame( '99', $result->getResultData( [ 'setValue' ] ) );
-
-               $result->addContentValue( null, 'setContentValue2', '99', ApiResult::OVERRIDE );
-               $this->assertSame( 'setContentValue2',
-                       $result->getResultData( [ ApiResult::META_CONTENT ] ) );
-
-               $result->reset();
-               $this->assertSame( [
-                       ApiResult::META_TYPE => 'assoc',
-               ], $result->getResultData() );
-               $this->assertSame( 0, $result->getSize() );
-
-               $result->addValue( null, 'foo', 1 );
-               $result->addValue( null, 'bar', 1 );
-               $result->addValue( null, 'top', '2', ApiResult::ADD_ON_TOP );
-               $result->addValue( null, null, '2', ApiResult::ADD_ON_TOP );
-               $result->addValue( null, 'bottom', '2' );
-               $result->addValue( null, 'foo', '2', ApiResult::OVERRIDE );
-               $result->addValue( null, 'bar', '2', ApiResult::OVERRIDE | ApiResult::ADD_ON_TOP );
-               $this->assertSame( [ 0, 'top', 'foo', 'bar', 'bottom', ApiResult::META_TYPE ],
-                       array_keys( $result->getResultData() ) );
-
-               $result->reset();
-               $result->addValue( null, 'foo', [ 'bar' => 1 ] );
-               $result->addValue( [ 'foo', 'top' ], 'x', 2, ApiResult::ADD_ON_TOP );
-               $result->addValue( [ 'foo', 'bottom' ], 'x', 2 );
-               $this->assertSame( [ 'top', 'bar', 'bottom' ],
-                       array_keys( $result->getResultData( [ 'foo' ] ) ) );
-
-               $result->reset();
-               $result->addValue( null, 'sub', [ 'foo' => 1 ] );
-               $result->addValue( null, 'sub', [ 'bar' => 1 ] );
-               $this->assertSame( [
-                       'sub' => [ 'foo' => 1, 'bar' => 1 ],
-                       ApiResult::META_TYPE => 'assoc',
-               ], $result->getResultData() );
-
-               try {
-                       $result->addValue( null, 'sub', [ 'foo' => 2, 'baz' => 2 ] );
-                       $this->fail( 'Expected exception not thrown' );
-               } catch ( RuntimeException $ex ) {
-                       $this->assertSame(
-                               'Conflicting keys (foo) when attempting to merge element sub',
-                               $ex->getMessage(),
-                               'Expected exception'
-                       );
-               }
-
-               $result->reset();
-               $title = Title::newFromText( "MediaWiki:Foobar" );
-               $obj = new stdClass;
-               $obj->foo = 1;
-               $obj->bar = 2;
-               $result->addValue( null, 'title', $title );
-               $result->addValue( null, 'obj', $obj );
-               $this->assertSame( [
-                       'title' => (string)$title,
-                       'obj' => [ 'foo' => 1, 'bar' => 2, ApiResult::META_TYPE => 'assoc' ],
-                       ApiResult::META_TYPE => 'assoc',
-               ], $result->getResultData() );
-
-               $fh = tmpfile();
-               try {
-                       $result->addValue( null, 'file', $fh );
-                       $this->fail( 'Expected exception not thrown' );
-               } catch ( InvalidArgumentException $ex ) {
-                       $this->assertSame(
-                               'Cannot add resource(stream) to ApiResult',
-                               $ex->getMessage(),
-                               'Expected exception'
-                       );
-               }
-               try {
-                       $result->addValue( null, null, $fh );
-                       $this->fail( 'Expected exception not thrown' );
-               } catch ( InvalidArgumentException $ex ) {
-                       $this->assertSame(
-                               'Cannot add resource(stream) to ApiResult',
-                               $ex->getMessage(),
-                               'Expected exception'
-                       );
-               }
-               try {
-                       $obj->file = $fh;
-                       $result->addValue( null, 'sub', $obj );
-                       $this->fail( 'Expected exception not thrown' );
-               } catch ( InvalidArgumentException $ex ) {
-                       $this->assertSame(
-                               'Cannot add resource(stream) to ApiResult',
-                               $ex->getMessage(),
-                               'Expected exception'
-                       );
-               }
-               try {
-                       $obj->file = $fh;
-                       $result->addValue( null, null, $obj );
-                       $this->fail( 'Expected exception not thrown' );
-               } catch ( InvalidArgumentException $ex ) {
-                       $this->assertSame(
-                               'Cannot add resource(stream) to ApiResult',
-                               $ex->getMessage(),
-                               'Expected exception'
-                       );
-               }
-               fclose( $fh );
-
-               try {
-                       $result->addValue( null, 'inf', INF );
-                       $this->fail( 'Expected exception not thrown' );
-               } catch ( InvalidArgumentException $ex ) {
-                       $this->assertSame(
-                               'Cannot add non-finite floats to ApiResult',
-                               $ex->getMessage(),
-                               'Expected exception'
-                       );
-               }
-               try {
-                       $result->addValue( null, null, INF );
-                       $this->fail( 'Expected exception not thrown' );
-               } catch ( InvalidArgumentException $ex ) {
-                       $this->assertSame(
-                               'Cannot add non-finite floats to ApiResult',
-                               $ex->getMessage(),
-                               'Expected exception'
-                       );
-               }
-               try {
-                       $result->addValue( null, 'nan', NAN );
-                       $this->fail( 'Expected exception not thrown' );
-               } catch ( InvalidArgumentException $ex ) {
-                       $this->assertSame(
-                               'Cannot add non-finite floats to ApiResult',
-                               $ex->getMessage(),
-                               'Expected exception'
-                       );
-               }
-               try {
-                       $result->addValue( null, null, NAN );
-                       $this->fail( 'Expected exception not thrown' );
-               } catch ( InvalidArgumentException $ex ) {
-                       $this->assertSame(
-                               'Cannot add non-finite floats to ApiResult',
-                               $ex->getMessage(),
-                               'Expected exception'
-                       );
-               }
-
-               $result->addValue( null, null, NAN, ApiResult::NO_VALIDATE );
-
-               try {
-                       $result->addValue( null, null, NAN, ApiResult::NO_SIZE_CHECK );
-                       $this->fail( 'Expected exception not thrown' );
-               } catch ( InvalidArgumentException $ex ) {
-                       $this->assertSame(
-                               'Cannot add non-finite floats to ApiResult',
-                               $ex->getMessage(),
-                               'Expected exception'
-                       );
-               }
-
-               $result->reset();
-               $result->addParsedLimit( 'foo', 12 );
-               $this->assertSame( [
-                       'limits' => [ 'foo' => 12 ],
-                       ApiResult::META_TYPE => 'assoc',
-               ], $result->getResultData() );
-               $result->addParsedLimit( 'foo', 13 );
-               $this->assertSame( [
-                       'limits' => [ 'foo' => 13 ],
-                       ApiResult::META_TYPE => 'assoc',
-               ], $result->getResultData() );
-               $this->assertSame( null, $result->getResultData( [ 'foo', 'bar', 'baz' ] ) );
-               $this->assertSame( 13, $result->getResultData( [ 'limits', 'foo' ] ) );
-               try {
-                       $result->getResultData( [ 'limits', 'foo', 'bar' ] );
-                       $this->fail( 'Expected exception not thrown' );
-               } catch ( InvalidArgumentException $ex ) {
-                       $this->assertSame(
-                               'Path limits.foo is not an array',
-                               $ex->getMessage(),
-                               'Expected exception'
-                       );
-               }
-
-               // Add two values and some metadata, but ensure metadata is not counted
-               $result = new ApiResult( 100 );
-               $obj = [ 'attr' => '12345' ];
-               ApiResult::setContentValue( $obj, 'content', '1234567890' );
-               $this->assertTrue( $result->addValue( null, 'foo', $obj ) );
-               $this->assertSame( 15, $result->getSize() );
-
-               $result = new ApiResult( 10 );
-               $formatter = new ApiErrorFormatter( $result, Language::factory( 'en' ), 'none', false );
-               $result->setErrorFormatter( $formatter );
-               $this->assertFalse( $result->addValue( null, 'foo', '12345678901' ) );
-               $this->assertTrue( $result->addValue( null, 'foo', '12345678901', ApiResult::NO_SIZE_CHECK ) );
-               $this->assertSame( 0, $result->getSize() );
-               $result->reset();
-               $this->assertTrue( $result->addValue( null, 'foo', '1234567890' ) );
-               $this->assertFalse( $result->addValue( null, 'foo', '1' ) );
-               $result->removeValue( null, 'foo' );
-               $this->assertTrue( $result->addValue( null, 'foo', '1' ) );
-
-               $result = new ApiResult( 10 );
-               $obj = new ApiResultTestSerializableObject( 'ok' );
-               $obj->foobar = 'foobaz';
-               $this->assertTrue( $result->addValue( null, 'foo', $obj ) );
-               $this->assertSame( 2, $result->getSize() );
-
-               $result = new ApiResult( 8388608 );
-               $result2 = new ApiResult( 8388608 );
-               $result2->addValue( null, 'foo', 'bar' );
-               $result->addValue( null, 'baz', $result2 );
-               $this->assertSame( [
-                       'baz' => [
-                               'foo' => 'bar',
-                               ApiResult::META_TYPE => 'assoc',
-                       ],
-                       ApiResult::META_TYPE => 'assoc',
-               ], $result->getResultData() );
-
-               $result = new ApiResult( 8388608 );
-               $result->addValue( null, 'foo', "foo\x80bar" );
-               $result->addValue( null, 'bar', "a\xcc\x81" );
-               $result->addValue( null, 'baz', 74 );
-               $result->addValue( null, null, "foo\x80bar" );
-               $result->addValue( null, null, "a\xcc\x81" );
-               $this->assertSame( [
-                       'foo' => "foo\xef\xbf\xbdbar",
-                       'bar' => "\xc3\xa1",
-                       'baz' => 74,
-                       0 => "foo\xef\xbf\xbdbar",
-                       1 => "\xc3\xa1",
-                       ApiResult::META_TYPE => 'assoc',
-               ], $result->getResultData() );
-
-               $result = new ApiResult( 8388608 );
-               $obj = new stdClass;
-               $obj->{'1'} = 'one';
-               $arr = [];
-               $result->addValue( $arr, 'foo', $obj );
-               $this->assertSame( [
-                       'foo' => [
-                               1 => 'one',
-                               ApiResult::META_TYPE => 'assoc',
-                       ],
-                       ApiResult::META_TYPE => 'assoc',
-               ], $result->getResultData() );
-       }
-
-       /**
-        * @covers ApiResult
-        */
-       public function testMetadata() {
-               $arr = [ 'foo' => [ 'bar' => [] ] ];
-               $result = new ApiResult( 8388608 );
-               $result->addValue( null, 'foo', [ 'bar' => [] ] );
-
-               $expect = [
-                       'foo' => [
-                               'bar' => [
-                                       ApiResult::META_INDEXED_TAG_NAME => 'ritn',
-                                       ApiResult::META_TYPE => 'default',
-                               ],
-                               ApiResult::META_INDEXED_TAG_NAME => 'ritn',
-                               ApiResult::META_TYPE => 'default',
-                       ],
-                       ApiResult::META_SUBELEMENTS => [ 'foo', 'bar' ],
-                       ApiResult::META_INDEXED_TAG_NAME => 'itn',
-                       ApiResult::META_PRESERVE_KEYS => [ 'foo', 'bar' ],
-                       ApiResult::META_TYPE => 'array',
-               ];
-
-               ApiResult::setSubelementsList( $arr, 'foo' );
-               ApiResult::setSubelementsList( $arr, [ 'bar', 'baz' ] );
-               ApiResult::unsetSubelementsList( $arr, 'baz' );
-               ApiResult::setIndexedTagNameRecursive( $arr, 'ritn' );
-               ApiResult::setIndexedTagName( $arr, 'itn' );
-               ApiResult::setPreserveKeysList( $arr, 'foo' );
-               ApiResult::setPreserveKeysList( $arr, [ 'bar', 'baz' ] );
-               ApiResult::unsetPreserveKeysList( $arr, 'baz' );
-               ApiResult::setArrayTypeRecursive( $arr, 'default' );
-               ApiResult::setArrayType( $arr, 'array' );
-               $this->assertSame( $expect, $arr );
-
-               $result->addSubelementsList( null, 'foo' );
-               $result->addSubelementsList( null, [ 'bar', 'baz' ] );
-               $result->removeSubelementsList( null, 'baz' );
-               $result->addIndexedTagNameRecursive( null, 'ritn' );
-               $result->addIndexedTagName( null, 'itn' );
-               $result->addPreserveKeysList( null, 'foo' );
-               $result->addPreserveKeysList( null, [ 'bar', 'baz' ] );
-               $result->removePreserveKeysList( null, 'baz' );
-               $result->addArrayTypeRecursive( null, 'default' );
-               $result->addArrayType( null, 'array' );
-               $this->assertEquals( $expect, $result->getResultData() );
-
-               $arr = [ 'foo' => [ 'bar' => [] ] ];
-               $expect = [
-                       'foo' => [
-                               'bar' => [
-                                       ApiResult::META_TYPE => 'kvp',
-                                       ApiResult::META_KVP_KEY_NAME => 'key',
-                               ],
-                               ApiResult::META_TYPE => 'kvp',
-                               ApiResult::META_KVP_KEY_NAME => 'key',
-                       ],
-                       ApiResult::META_TYPE => 'BCkvp',
-                       ApiResult::META_KVP_KEY_NAME => 'bc',
-               ];
-               ApiResult::setArrayTypeRecursive( $arr, 'kvp', 'key' );
-               ApiResult::setArrayType( $arr, 'BCkvp', 'bc' );
-               $this->assertSame( $expect, $arr );
-       }
-
-       /**
-        * @covers ApiResult
-        */
-       public function testUtilityFunctions() {
-               $arr = [
-                       'foo' => [
-                               'bar' => [ '_dummy' => 'foobaz' ],
-                               'bar2' => (object)[ '_dummy' => 'foobaz' ],
-                               'x' => 'ok',
-                               '_dummy' => 'foobaz',
-                       ],
-                       'foo2' => (object)[
-                               'bar' => [ '_dummy' => 'foobaz' ],
-                               'bar2' => (object)[ '_dummy' => 'foobaz' ],
-                               'x' => 'ok',
-                               '_dummy' => 'foobaz',
-                       ],
-                       ApiResult::META_SUBELEMENTS => [ 'foo', 'bar' ],
-                       ApiResult::META_INDEXED_TAG_NAME => 'itn',
-                       ApiResult::META_PRESERVE_KEYS => [ 'foo', 'bar', '_dummy2', 0 ],
-                       ApiResult::META_TYPE => 'array',
-                       '_dummy' => 'foobaz',
-                       '_dummy2' => 'foobaz!',
-               ];
-               $this->assertEquals( [
-                       'foo' => [
-                               'bar' => [],
-                               'bar2' => (object)[],
-                               'x' => 'ok',
-                       ],
-                       'foo2' => (object)[
-                               'bar' => [],
-                               'bar2' => (object)[],
-                               'x' => 'ok',
-                       ],
-                       '_dummy2' => 'foobaz!',
-               ], ApiResult::stripMetadata( $arr ), 'ApiResult::stripMetadata' );
-
-               $metadata = [];
-               $data = ApiResult::stripMetadataNonRecursive( $arr, $metadata );
-               $this->assertEquals( [
-                       'foo' => [
-                               'bar' => [ '_dummy' => 'foobaz' ],
-                               'bar2' => (object)[ '_dummy' => 'foobaz' ],
-                               'x' => 'ok',
-                               '_dummy' => 'foobaz',
-                       ],
-                       'foo2' => (object)[
-                               'bar' => [ '_dummy' => 'foobaz' ],
-                               'bar2' => (object)[ '_dummy' => 'foobaz' ],
-                               'x' => 'ok',
-                               '_dummy' => 'foobaz',
-                       ],
-                       '_dummy2' => 'foobaz!',
-               ], $data, 'ApiResult::stripMetadataNonRecursive ($data)' );
-               $this->assertEquals( [
-                       ApiResult::META_SUBELEMENTS => [ 'foo', 'bar' ],
-                       ApiResult::META_INDEXED_TAG_NAME => 'itn',
-                       ApiResult::META_PRESERVE_KEYS => [ 'foo', 'bar', '_dummy2', 0 ],
-                       ApiResult::META_TYPE => 'array',
-                       '_dummy' => 'foobaz',
-               ], $metadata, 'ApiResult::stripMetadataNonRecursive ($metadata)' );
-
-               $metadata = null;
-               $data = ApiResult::stripMetadataNonRecursive( (object)$arr, $metadata );
-               $this->assertEquals( (object)[
-                       'foo' => [
-                               'bar' => [ '_dummy' => 'foobaz' ],
-                               'bar2' => (object)[ '_dummy' => 'foobaz' ],
-                               'x' => 'ok',
-                               '_dummy' => 'foobaz',
-                       ],
-                       'foo2' => (object)[
-                               'bar' => [ '_dummy' => 'foobaz' ],
-                               'bar2' => (object)[ '_dummy' => 'foobaz' ],
-                               'x' => 'ok',
-                               '_dummy' => 'foobaz',
-                       ],
-                       '_dummy2' => 'foobaz!',
-               ], $data, 'ApiResult::stripMetadataNonRecursive on object ($data)' );
-               $this->assertEquals( [
-                       ApiResult::META_SUBELEMENTS => [ 'foo', 'bar' ],
-                       ApiResult::META_INDEXED_TAG_NAME => 'itn',
-                       ApiResult::META_PRESERVE_KEYS => [ 'foo', 'bar', '_dummy2', 0 ],
-                       ApiResult::META_TYPE => 'array',
-                       '_dummy' => 'foobaz',
-               ], $metadata, 'ApiResult::stripMetadataNonRecursive on object ($metadata)' );
-       }
-
-       /**
-        * @covers ApiResult
-        * @dataProvider provideTransformations
-        * @param string $label
-        * @param array $input
-        * @param array $transforms
-        * @param array|Exception $expect
-        */
-       public function testTransformations( $label, $input, $transforms, $expect ) {
-               $result = new ApiResult( false );
-               $result->addValue( null, 'test', $input );
-
-               if ( $expect instanceof Exception ) {
-                       try {
-                               $output = $result->getResultData( 'test', $transforms );
-                               $this->fail( 'Expected exception not thrown', $label );
-                       } catch ( Exception $ex ) {
-                               $this->assertEquals( $ex, $expect, $label );
-                       }
-               } else {
-                       $output = $result->getResultData( 'test', $transforms );
-                       $this->assertEquals( $expect, $output, $label );
-               }
-       }
-
-       public function provideTransformations() {
-               $kvp = function ( $keyKey, $key, $valKey, $value ) {
-                       return [
-                               $keyKey => $key,
-                               $valKey => $value,
-                               ApiResult::META_PRESERVE_KEYS => [ $keyKey ],
-                               ApiResult::META_CONTENT => $valKey,
-                               ApiResult::META_TYPE => 'assoc',
-                       ];
-               };
-               $typeArr = [
-                       'defaultArray' => [ 2 => 'a', 0 => 'b', 1 => 'c' ],
-                       'defaultAssoc' => [ 'x' => 'a', 1 => 'b', 0 => 'c' ],
-                       'defaultAssoc2' => [ 2 => 'a', 3 => 'b', 0 => 'c' ],
-                       'array' => [ 'x' => 'a', 1 => 'b', 0 => 'c', ApiResult::META_TYPE => 'array' ],
-                       'BCarray' => [ 'x' => 'a', 1 => 'b', 0 => 'c', ApiResult::META_TYPE => 'BCarray' ],
-                       'BCassoc' => [ 'a', 'b', 'c', ApiResult::META_TYPE => 'BCassoc' ],
-                       'assoc' => [ 2 => 'a', 0 => 'b', 1 => 'c', ApiResult::META_TYPE => 'assoc' ],
-                       'kvp' => [ 'x' => 'a', 'y' => 'b', 'z' => [ 'c' ], ApiResult::META_TYPE => 'kvp' ],
-                       'BCkvp' => [ 'x' => 'a', 'y' => 'b',
-                               ApiResult::META_TYPE => 'BCkvp',
-                               ApiResult::META_KVP_KEY_NAME => 'key',
-                       ],
-                       'kvpmerge' => [ 'x' => 'a', 'y' => [ 'b' ], 'z' => [ 'c' => 'd' ],
-                               ApiResult::META_TYPE => 'kvp',
-                               ApiResult::META_KVP_MERGE => true,
-                       ],
-                       'emptyDefault' => [ '_dummy' => 1 ],
-                       'emptyAssoc' => [ '_dummy' => 1, ApiResult::META_TYPE => 'assoc' ],
-                       '_dummy' => 1,
-                       ApiResult::META_PRESERVE_KEYS => [ '_dummy' ],
-               ];
-               $stripArr = [
-                       'foo' => [
-                               'bar' => [ '_dummy' => 'foobaz' ],
-                               'baz' => [
-                                       ApiResult::META_SUBELEMENTS => [ 'foo', 'bar' ],
-                                       ApiResult::META_INDEXED_TAG_NAME => 'itn',
-                                       ApiResult::META_PRESERVE_KEYS => [ 'foo', 'bar', '_dummy2', 0 ],
-                                       ApiResult::META_TYPE => 'array',
-                               ],
-                               'x' => 'ok',
-                               '_dummy' => 'foobaz',
-                       ],
-                       ApiResult::META_SUBELEMENTS => [ 'foo', 'bar' ],
-                       ApiResult::META_INDEXED_TAG_NAME => 'itn',
-                       ApiResult::META_PRESERVE_KEYS => [ 'foo', 'bar', '_dummy2', 0 ],
-                       ApiResult::META_TYPE => 'array',
-                       '_dummy' => 'foobaz',
-                       '_dummy2' => 'foobaz!',
-               ];
-
-               return [
-                       [
-                               'BC: META_BC_BOOLS',
-                               [
-                                       'BCtrue' => true,
-                                       'BCfalse' => false,
-                                       'true' => true,
-                                       'false' => false,
-                                       ApiResult::META_BC_BOOLS => [ 0, 'true', 'false' ],
-                               ],
-                               [ 'BC' => [] ],
-                               [
-                                       'BCtrue' => '',
-                                       'true' => true,
-                                       'false' => false,
-                                       ApiResult::META_BC_BOOLS => [ 0, 'true', 'false' ],
-                               ]
-                       ],
-                       [
-                               'BC: META_BC_SUBELEMENTS',
-                               [
-                                       'bc' => 'foo',
-                                       'nobc' => 'bar',
-                                       ApiResult::META_BC_SUBELEMENTS => [ 'bc' ],
-                               ],
-                               [ 'BC' => [] ],
-                               [
-                                       'bc' => [
-                                               '*' => 'foo',
-                                               ApiResult::META_CONTENT => '*',
-                                               ApiResult::META_TYPE => 'assoc',
-                                       ],
-                                       'nobc' => 'bar',
-                                       ApiResult::META_BC_SUBELEMENTS => [ 'bc' ],
-                               ],
-                       ],
-                       [
-                               'BC: META_CONTENT',
-                               [
-                                       'content' => '!!!',
-                                       ApiResult::META_CONTENT => 'content',
-                               ],
-                               [ 'BC' => [] ],
-                               [
-                                       '*' => '!!!',
-                                       ApiResult::META_CONTENT => '*',
-                               ],
-                       ],
-                       [
-                               'BC: BCkvp type',
-                               [
-                                       'foo' => 'foo value',
-                                       'bar' => 'bar value',
-                                       '_baz' => 'baz value',
-                                       ApiResult::META_TYPE => 'BCkvp',
-                                       ApiResult::META_KVP_KEY_NAME => 'key',
-                                       ApiResult::META_PRESERVE_KEYS => [ '_baz' ],
-                               ],
-                               [ 'BC' => [] ],
-                               [
-                                       $kvp( 'key', 'foo', '*', 'foo value' ),
-                                       $kvp( 'key', 'bar', '*', 'bar value' ),
-                                       $kvp( 'key', '_baz', '*', 'baz value' ),
-                                       ApiResult::META_TYPE => 'array',
-                                       ApiResult::META_KVP_KEY_NAME => 'key',
-                                       ApiResult::META_PRESERVE_KEYS => [ '_baz' ],
-                               ],
-                       ],
-                       [
-                               'BC: BCarray type',
-                               [
-                                       ApiResult::META_TYPE => 'BCarray',
-                               ],
-                               [ 'BC' => [] ],
-                               [
-                                       ApiResult::META_TYPE => 'default',
-                               ],
-                       ],
-                       [
-                               'BC: BCassoc type',
-                               [
-                                       ApiResult::META_TYPE => 'BCassoc',
-                               ],
-                               [ 'BC' => [] ],
-                               [
-                                       ApiResult::META_TYPE => 'default',
-                               ],
-                       ],
-                       [
-                               'BC: BCkvp exception',
-                               [
-                                       ApiResult::META_TYPE => 'BCkvp',
-                               ],
-                               [ 'BC' => [] ],
-                               new UnexpectedValueException(
-                                       'Type "BCkvp" used without setting ApiResult::META_KVP_KEY_NAME metadata item'
-                               ),
-                       ],
-                       [
-                               'BC: nobool, no*, nosub',
-                               [
-                                       'true' => true,
-                                       'false' => false,
-                                       'content' => 'content',
-                                       ApiResult::META_CONTENT => 'content',
-                                       'bc' => 'foo',
-                                       ApiResult::META_BC_SUBELEMENTS => [ 'bc' ],
-                                       'BCarray' => [ ApiResult::META_TYPE => 'BCarray' ],
-                                       'BCassoc' => [ ApiResult::META_TYPE => 'BCassoc' ],
-                                       'BCkvp' => [
-                                               'foo' => 'foo value',
-                                               'bar' => 'bar value',
-                                               '_baz' => 'baz value',
-                                               ApiResult::META_TYPE => 'BCkvp',
-                                               ApiResult::META_KVP_KEY_NAME => 'key',
-                                               ApiResult::META_PRESERVE_KEYS => [ '_baz' ],
-                                       ],
-                               ],
-                               [ 'BC' => [ 'nobool', 'no*', 'nosub' ] ],
-                               [
-                                       'true' => true,
-                                       'false' => false,
-                                       'content' => 'content',
-                                       'bc' => 'foo',
-                                       'BCarray' => [ ApiResult::META_TYPE => 'default' ],
-                                       'BCassoc' => [ ApiResult::META_TYPE => 'default' ],
-                                       'BCkvp' => [
-                                               $kvp( 'key', 'foo', '*', 'foo value' ),
-                                               $kvp( 'key', 'bar', '*', 'bar value' ),
-                                               $kvp( 'key', '_baz', '*', 'baz value' ),
-                                               ApiResult::META_TYPE => 'array',
-                                               ApiResult::META_KVP_KEY_NAME => 'key',
-                                               ApiResult::META_PRESERVE_KEYS => [ '_baz' ],
-                                       ],
-                                       ApiResult::META_CONTENT => 'content',
-                                       ApiResult::META_BC_SUBELEMENTS => [ 'bc' ],
-                               ],
-                       ],
-
-                       [
-                               'Types: Normal transform',
-                               $typeArr,
-                               [ 'Types' => [] ],
-                               [
-                                       'defaultArray' => [ 'b', 'c', 'a', ApiResult::META_TYPE => 'array' ],
-                                       'defaultAssoc' => [ 'x' => 'a', 1 => 'b', 0 => 'c', ApiResult::META_TYPE => 'assoc' ],
-                                       'defaultAssoc2' => [ 2 => 'a', 3 => 'b', 0 => 'c', ApiResult::META_TYPE => 'assoc' ],
-                                       'array' => [ 'a', 'c', 'b', ApiResult::META_TYPE => 'array' ],
-                                       'BCarray' => [ 'a', 'c', 'b', ApiResult::META_TYPE => 'array' ],
-                                       'BCassoc' => [ 'a', 'b', 'c', ApiResult::META_TYPE => 'assoc' ],
-                                       'assoc' => [ 2 => 'a', 0 => 'b', 1 => 'c', ApiResult::META_TYPE => 'assoc' ],
-                                       'kvp' => [ 'x' => 'a', 'y' => 'b',
-                                               'z' => [ 'c', ApiResult::META_TYPE => 'array' ],
-                                               ApiResult::META_TYPE => 'assoc'
-                                       ],
-                                       'BCkvp' => [ 'x' => 'a', 'y' => 'b',
-                                               ApiResult::META_TYPE => 'assoc',
-                                               ApiResult::META_KVP_KEY_NAME => 'key',
-                                       ],
-                                       'kvpmerge' => [
-                                               'x' => 'a',
-                                               'y' => [ 'b', ApiResult::META_TYPE => 'array' ],
-                                               'z' => [ 'c' => 'd', ApiResult::META_TYPE => 'assoc' ],
-                                               ApiResult::META_TYPE => 'assoc',
-                                               ApiResult::META_KVP_MERGE => true,
-                                       ],
-                                       'emptyDefault' => [ '_dummy' => 1, ApiResult::META_TYPE => 'array' ],
-                                       'emptyAssoc' => [ '_dummy' => 1, ApiResult::META_TYPE => 'assoc' ],
-                                       '_dummy' => 1,
-                                       ApiResult::META_PRESERVE_KEYS => [ '_dummy' ],
-                                       ApiResult::META_TYPE => 'assoc',
-                               ],
-                       ],
-                       [
-                               'Types: AssocAsObject',
-                               $typeArr,
-                               [ 'Types' => [ 'AssocAsObject' => true ] ],
-                               (object)[
-                                       'defaultArray' => [ 'b', 'c', 'a', ApiResult::META_TYPE => 'array' ],
-                                       'defaultAssoc' => (object)[ 'x' => 'a',
-                                               1 => 'b', 0 => 'c', ApiResult::META_TYPE => 'assoc'
-                                       ],
-                                       'defaultAssoc2' => (object)[ 2 => 'a', 3 => 'b',
-                                               0 => 'c', ApiResult::META_TYPE => 'assoc'
-                                       ],
-                                       'array' => [ 'a', 'c', 'b', ApiResult::META_TYPE => 'array' ],
-                                       'BCarray' => [ 'a', 'c', 'b', ApiResult::META_TYPE => 'array' ],
-                                       'BCassoc' => (object)[ 'a', 'b', 'c', ApiResult::META_TYPE => 'assoc' ],
-                                       'assoc' => (object)[ 2 => 'a', 0 => 'b', 1 => 'c', ApiResult::META_TYPE => 'assoc' ],
-                                       'kvp' => (object)[ 'x' => 'a', 'y' => 'b',
-                                               'z' => [ 'c', ApiResult::META_TYPE => 'array' ],
-                                               ApiResult::META_TYPE => 'assoc'
-                                       ],
-                                       'BCkvp' => (object)[ 'x' => 'a', 'y' => 'b',
-                                               ApiResult::META_TYPE => 'assoc',
-                                               ApiResult::META_KVP_KEY_NAME => 'key',
-                                       ],
-                                       'kvpmerge' => (object)[
-                                               'x' => 'a',
-                                               'y' => [ 'b', ApiResult::META_TYPE => 'array' ],
-                                               'z' => (object)[ 'c' => 'd', ApiResult::META_TYPE => 'assoc' ],
-                                               ApiResult::META_TYPE => 'assoc',
-                                               ApiResult::META_KVP_MERGE => true,
-                                       ],
-                                       'emptyDefault' => [ '_dummy' => 1, ApiResult::META_TYPE => 'array' ],
-                                       'emptyAssoc' => (object)[ '_dummy' => 1, ApiResult::META_TYPE => 'assoc' ],
-                                       '_dummy' => 1,
-                                       ApiResult::META_PRESERVE_KEYS => [ '_dummy' ],
-                                       ApiResult::META_TYPE => 'assoc',
-                               ],
-                       ],
-                       [
-                               'Types: ArmorKVP',
-                               $typeArr,
-                               [ 'Types' => [ 'ArmorKVP' => 'name' ] ],
-                               [
-                                       'defaultArray' => [ 'b', 'c', 'a', ApiResult::META_TYPE => 'array' ],
-                                       'defaultAssoc' => [ 'x' => 'a', 1 => 'b', 0 => 'c', ApiResult::META_TYPE => 'assoc' ],
-                                       'defaultAssoc2' => [ 2 => 'a', 3 => 'b', 0 => 'c', ApiResult::META_TYPE => 'assoc' ],
-                                       'array' => [ 'a', 'c', 'b', ApiResult::META_TYPE => 'array' ],
-                                       'BCarray' => [ 'a', 'c', 'b', ApiResult::META_TYPE => 'array' ],
-                                       'BCassoc' => [ 'a', 'b', 'c', ApiResult::META_TYPE => 'assoc' ],
-                                       'assoc' => [ 2 => 'a', 0 => 'b', 1 => 'c', ApiResult::META_TYPE => 'assoc' ],
-                                       'kvp' => [
-                                               $kvp( 'name', 'x', 'value', 'a' ),
-                                               $kvp( 'name', 'y', 'value', 'b' ),
-                                               $kvp( 'name', 'z', 'value', [ 'c', ApiResult::META_TYPE => 'array' ] ),
-                                               ApiResult::META_TYPE => 'array'
-                                       ],
-                                       'BCkvp' => [
-                                               $kvp( 'key', 'x', 'value', 'a' ),
-                                               $kvp( 'key', 'y', 'value', 'b' ),
-                                               ApiResult::META_TYPE => 'array',
-                                               ApiResult::META_KVP_KEY_NAME => 'key',
-                                       ],
-                                       'kvpmerge' => [
-                                               $kvp( 'name', 'x', 'value', 'a' ),
-                                               $kvp( 'name', 'y', 'value', [ 'b', ApiResult::META_TYPE => 'array' ] ),
-                                               [
-                                                       'name' => 'z',
-                                                       'c' => 'd',
-                                                       ApiResult::META_TYPE => 'assoc',
-                                                       ApiResult::META_PRESERVE_KEYS => [ 'name' ]
-                                               ],
-                                               ApiResult::META_TYPE => 'array',
-                                               ApiResult::META_KVP_MERGE => true,
-                                       ],
-                                       'emptyDefault' => [ '_dummy' => 1, ApiResult::META_TYPE => 'array' ],
-                                       'emptyAssoc' => [ '_dummy' => 1, ApiResult::META_TYPE => 'assoc' ],
-                                       '_dummy' => 1,
-                                       ApiResult::META_PRESERVE_KEYS => [ '_dummy' ],
-                                       ApiResult::META_TYPE => 'assoc',
-                               ],
-                       ],
-                       [
-                               'Types: ArmorKVP + BC',
-                               $typeArr,
-                               [ 'BC' => [], 'Types' => [ 'ArmorKVP' => 'name' ] ],
-                               [
-                                       'defaultArray' => [ 'b', 'c', 'a', ApiResult::META_TYPE => 'array' ],
-                                       'defaultAssoc' => [ 'x' => 'a', 1 => 'b', 0 => 'c', ApiResult::META_TYPE => 'assoc' ],
-                                       'defaultAssoc2' => [ 2 => 'a', 3 => 'b', 0 => 'c', ApiResult::META_TYPE => 'assoc' ],
-                                       'array' => [ 'a', 'c', 'b', ApiResult::META_TYPE => 'array' ],
-                                       'BCarray' => [ 'x' => 'a', 1 => 'b', 0 => 'c', ApiResult::META_TYPE => 'assoc' ],
-                                       'BCassoc' => [ 'a', 'b', 'c', ApiResult::META_TYPE => 'array' ],
-                                       'assoc' => [ 2 => 'a', 0 => 'b', 1 => 'c', ApiResult::META_TYPE => 'assoc' ],
-                                       'kvp' => [
-                                               $kvp( 'name', 'x', '*', 'a' ),
-                                               $kvp( 'name', 'y', '*', 'b' ),
-                                               $kvp( 'name', 'z', '*', [ 'c', ApiResult::META_TYPE => 'array' ] ),
-                                               ApiResult::META_TYPE => 'array'
-                                       ],
-                                       'BCkvp' => [
-                                               $kvp( 'key', 'x', '*', 'a' ),
-                                               $kvp( 'key', 'y', '*', 'b' ),
-                                               ApiResult::META_TYPE => 'array',
-                                               ApiResult::META_KVP_KEY_NAME => 'key',
-                                       ],
-                                       'kvpmerge' => [
-                                               $kvp( 'name', 'x', '*', 'a' ),
-                                               $kvp( 'name', 'y', '*', [ 'b', ApiResult::META_TYPE => 'array' ] ),
-                                               [
-                                                       'name' => 'z',
-                                                       'c' => 'd',
-                                                       ApiResult::META_TYPE => 'assoc',
-                                                       ApiResult::META_PRESERVE_KEYS => [ 'name' ] ],
-                                               ApiResult::META_TYPE => 'array',
-                                               ApiResult::META_KVP_MERGE => true,
-                                       ],
-                                       'emptyDefault' => [ '_dummy' => 1, ApiResult::META_TYPE => 'array' ],
-                                       'emptyAssoc' => [ '_dummy' => 1, ApiResult::META_TYPE => 'assoc' ],
-                                       '_dummy' => 1,
-                                       ApiResult::META_PRESERVE_KEYS => [ '_dummy' ],
-                                       ApiResult::META_TYPE => 'assoc',
-                               ],
-                       ],
-                       [
-                               'Types: ArmorKVP + AssocAsObject',
-                               $typeArr,
-                               [ 'Types' => [ 'ArmorKVP' => 'name', 'AssocAsObject' => true ] ],
-                               (object)[
-                                       'defaultArray' => [ 'b', 'c', 'a', ApiResult::META_TYPE => 'array' ],
-                                       'defaultAssoc' => (object)[ 'x' => 'a', 1 => 'b',
-                                               0 => 'c', ApiResult::META_TYPE => 'assoc'
-                                       ],
-                                       'defaultAssoc2' => (object)[ 2 => 'a', 3 => 'b',
-                                               0 => 'c', ApiResult::META_TYPE => 'assoc'
-                                       ],
-                                       'array' => [ 'a', 'c', 'b', ApiResult::META_TYPE => 'array' ],
-                                       'BCarray' => [ 'a', 'c', 'b', ApiResult::META_TYPE => 'array' ],
-                                       'BCassoc' => (object)[ 'a', 'b', 'c', ApiResult::META_TYPE => 'assoc' ],
-                                       'assoc' => (object)[ 2 => 'a', 0 => 'b', 1 => 'c', ApiResult::META_TYPE => 'assoc' ],
-                                       'kvp' => [
-                                               (object)$kvp( 'name', 'x', 'value', 'a' ),
-                                               (object)$kvp( 'name', 'y', 'value', 'b' ),
-                                               (object)$kvp( 'name', 'z', 'value', [ 'c', ApiResult::META_TYPE => 'array' ] ),
-                                               ApiResult::META_TYPE => 'array'
-                                       ],
-                                       'BCkvp' => [
-                                               (object)$kvp( 'key', 'x', 'value', 'a' ),
-                                               (object)$kvp( 'key', 'y', 'value', 'b' ),
-                                               ApiResult::META_TYPE => 'array',
-                                               ApiResult::META_KVP_KEY_NAME => 'key',
-                                       ],
-                                       'kvpmerge' => [
-                                               (object)$kvp( 'name', 'x', 'value', 'a' ),
-                                               (object)$kvp( 'name', 'y', 'value', [ 'b', ApiResult::META_TYPE => 'array' ] ),
-                                               (object)[
-                                                       'name' => 'z',
-                                                       'c' => 'd',
-                                                       ApiResult::META_TYPE => 'assoc',
-                                                       ApiResult::META_PRESERVE_KEYS => [ 'name' ]
-                                               ],
-                                               ApiResult::META_TYPE => 'array',
-                                               ApiResult::META_KVP_MERGE => true,
-                                       ],
-                                       'emptyDefault' => [ '_dummy' => 1, ApiResult::META_TYPE => 'array' ],
-                                       'emptyAssoc' => (object)[ '_dummy' => 1, ApiResult::META_TYPE => 'assoc' ],
-                                       '_dummy' => 1,
-                                       ApiResult::META_PRESERVE_KEYS => [ '_dummy' ],
-                                       ApiResult::META_TYPE => 'assoc',
-                               ],
-                       ],
-                       [
-                               'Types: BCkvp exception',
-                               [
-                                       ApiResult::META_TYPE => 'BCkvp',
-                               ],
-                               [ 'Types' => [] ],
-                               new UnexpectedValueException(
-                                       'Type "BCkvp" used without setting ApiResult::META_KVP_KEY_NAME metadata item'
-                               ),
-                       ],
-
-                       [
-                               'Strip: With ArmorKVP + AssocAsObject transforms',
-                               $typeArr,
-                               [ 'Types' => [ 'ArmorKVP' => 'name', 'AssocAsObject' => true ], 'Strip' => 'all' ],
-                               (object)[
-                                       'defaultArray' => [ 'b', 'c', 'a' ],
-                                       'defaultAssoc' => (object)[ 'x' => 'a', 1 => 'b', 0 => 'c' ],
-                                       'defaultAssoc2' => (object)[ 2 => 'a', 3 => 'b', 0 => 'c' ],
-                                       'array' => [ 'a', 'c', 'b' ],
-                                       'BCarray' => [ 'a', 'c', 'b' ],
-                                       'BCassoc' => (object)[ 'a', 'b', 'c' ],
-                                       'assoc' => (object)[ 2 => 'a', 0 => 'b', 1 => 'c' ],
-                                       'kvp' => [
-                                               (object)[ 'name' => 'x', 'value' => 'a' ],
-                                               (object)[ 'name' => 'y', 'value' => 'b' ],
-                                               (object)[ 'name' => 'z', 'value' => [ 'c' ] ],
-                                       ],
-                                       'BCkvp' => [
-                                               (object)[ 'key' => 'x', 'value' => 'a' ],
-                                               (object)[ 'key' => 'y', 'value' => 'b' ],
-                                       ],
-                                       'kvpmerge' => [
-                                               (object)[ 'name' => 'x', 'value' => 'a' ],
-                                               (object)[ 'name' => 'y', 'value' => [ 'b' ] ],
-                                               (object)[ 'name' => 'z', 'c' => 'd' ],
-                                       ],
-                                       'emptyDefault' => [],
-                                       'emptyAssoc' => (object)[],
-                                       '_dummy' => 1,
-                               ],
-                       ],
-
-                       [
-                               'Strip: all',
-                               $stripArr,
-                               [ 'Strip' => 'all' ],
-                               [
-                                       'foo' => [
-                                               'bar' => [],
-                                               'baz' => [],
-                                               'x' => 'ok',
-                                       ],
-                                       '_dummy2' => 'foobaz!',
-                               ],
-                       ],
-                       [
-                               'Strip: base',
-                               $stripArr,
-                               [ 'Strip' => 'base' ],
-                               [
-                                       'foo' => [
-                                               'bar' => [ '_dummy' => 'foobaz' ],
-                                               'baz' => [
-                                                       ApiResult::META_SUBELEMENTS => [ 'foo', 'bar' ],
-                                                       ApiResult::META_INDEXED_TAG_NAME => 'itn',
-                                                       ApiResult::META_PRESERVE_KEYS => [ 'foo', 'bar', '_dummy2', 0 ],
-                                                       ApiResult::META_TYPE => 'array',
-                                               ],
-                                               'x' => 'ok',
-                                               '_dummy' => 'foobaz',
-                                       ],
-                                       '_dummy2' => 'foobaz!',
-                               ],
-                       ],
-                       [
-                               'Strip: bc',
-                               $stripArr,
-                               [ 'Strip' => 'bc' ],
-                               [
-                                       'foo' => [
-                                               'bar' => [],
-                                               'baz' => [
-                                                       ApiResult::META_SUBELEMENTS => [ 'foo', 'bar' ],
-                                                       ApiResult::META_INDEXED_TAG_NAME => 'itn',
-                                               ],
-                                               'x' => 'ok',
-                                       ],
-                                       '_dummy2' => 'foobaz!',
-                                       ApiResult::META_SUBELEMENTS => [ 'foo', 'bar' ],
-                                       ApiResult::META_INDEXED_TAG_NAME => 'itn',
-                               ],
-                       ],
-
-                       [
-                               'Custom transform',
-                               [
-                                       'foo' => '?',
-                                       'bar' => '?',
-                                       '_dummy' => '?',
-                                       '_dummy2' => '?',
-                                       '_dummy3' => '?',
-                                       ApiResult::META_CONTENT => 'foo',
-                                       ApiResult::META_PRESERVE_KEYS => [ '_dummy2', '_dummy3' ],
-                               ],
-                               [
-                                       'Custom' => [ $this, 'customTransform' ],
-                                       'BC' => [],
-                                       'Types' => [],
-                                       'Strip' => 'all'
-                               ],
-                               [
-                                       '*' => 'FOO',
-                                       'bar' => 'BAR',
-                                       'baz' => [ 'a', 'b' ],
-                                       '_dummy2' => '_DUMMY2',
-                                       '_dummy3' => '_DUMMY3',
-                                       ApiResult::META_CONTENT => 'bar',
-                               ],
-                       ],
-               ];
-       }
-
-       /**
-        * Custom transformer for testTransformations
-        * @param array &$data
-        * @param array &$metadata
-        */
-       public function customTransform( &$data, &$metadata ) {
-               // Prevent recursion
-               if ( isset( $metadata['_added'] ) ) {
-                       $metadata[ApiResult::META_TYPE] = 'array';
-                       return;
-               }
-
-               foreach ( $data as $k => $v ) {
-                       $data[$k] = strtoupper( $k );
-               }
-               $data['baz'] = [ '_added' => 1, 'z' => 'b', 'y' => 'a' ];
-               $metadata[ApiResult::META_PRESERVE_KEYS][0] = '_dummy';
-               $data[ApiResult::META_CONTENT] = 'bar';
-       }
-
-       /**
-        * @covers ApiResult
-        */
-       public function testAddMetadataToResultVars() {
-               $arr = [
-                       'a' => "foo",
-                       'b' => false,
-                       'c' => 10,
-                       'sequential_numeric_keys' => [ 'a', 'b', 'c' ],
-                       'non_sequential_numeric_keys' => [ 'a', 'b', 4 => 'c' ],
-                       'string_keys' => [
-                               'one' => 1,
-                               'two' => 2
-                       ],
-                       'object_sequential_keys' => (object)[ 'a', 'b', 'c' ],
-                       '_type' => "should be overwritten in result",
-               ];
-               $this->assertSame( [
-                       ApiResult::META_TYPE => 'kvp',
-                       ApiResult::META_KVP_KEY_NAME => 'key',
-                       ApiResult::META_PRESERVE_KEYS => [
-                               'a', 'b', 'c',
-                               'sequential_numeric_keys', 'non_sequential_numeric_keys',
-                               'string_keys', 'object_sequential_keys'
-                       ],
-                       ApiResult::META_BC_BOOLS => [ 'b' ],
-                       ApiResult::META_INDEXED_TAG_NAME => 'var',
-                       'a' => "foo",
-                       'b' => false,
-                       'c' => 10,
-                       'sequential_numeric_keys' => [
-                               ApiResult::META_TYPE => 'array',
-                               ApiResult::META_BC_BOOLS => [],
-                               ApiResult::META_INDEXED_TAG_NAME => 'value',
-                               0 => 'a',
-                               1 => 'b',
-                               2 => 'c',
-                       ],
-                       'non_sequential_numeric_keys' => [
-                               ApiResult::META_TYPE => 'kvp',
-                               ApiResult::META_KVP_KEY_NAME => 'key',
-                               ApiResult::META_PRESERVE_KEYS => [ 0, 1, 4 ],
-                               ApiResult::META_BC_BOOLS => [],
-                               ApiResult::META_INDEXED_TAG_NAME => 'var',
-                               0 => 'a',
-                               1 => 'b',
-                               4 => 'c',
-                       ],
-                       'string_keys' => [
-                               ApiResult::META_TYPE => 'kvp',
-                               ApiResult::META_KVP_KEY_NAME => 'key',
-                               ApiResult::META_PRESERVE_KEYS => [ 'one', 'two' ],
-                               ApiResult::META_BC_BOOLS => [],
-                               ApiResult::META_INDEXED_TAG_NAME => 'var',
-                               'one' => 1,
-                               'two' => 2,
-                       ],
-                       'object_sequential_keys' => [
-                               ApiResult::META_TYPE => 'kvp',
-                               ApiResult::META_KVP_KEY_NAME => 'key',
-                               ApiResult::META_PRESERVE_KEYS => [ 0, 1, 2 ],
-                               ApiResult::META_BC_BOOLS => [],
-                               ApiResult::META_INDEXED_TAG_NAME => 'var',
-                               0 => 'a',
-                               1 => 'b',
-                               2 => 'c',
-                       ],
-               ], ApiResult::addMetadataToResultVars( $arr ) );
-       }
-
-       public function testObjectSerialization() {
-               $arr = [];
-               ApiResult::setValue( $arr, 'foo', (object)[ 'a' => 1, 'b' => 2 ] );
-               $this->assertSame( [
-                       'a' => 1,
-                       'b' => 2,
-                       ApiResult::META_TYPE => 'assoc',
-               ], $arr['foo'] );
-
-               $arr = [];
-               ApiResult::setValue( $arr, 'foo', new ApiResultTestStringifiableObject() );
-               $this->assertSame( 'Ok', $arr['foo'] );
-
-               $arr = [];
-               ApiResult::setValue( $arr, 'foo', new ApiResultTestSerializableObject( 'Ok' ) );
-               $this->assertSame( 'Ok', $arr['foo'] );
-
-               try {
-                       $arr = [];
-                       ApiResult::setValue( $arr, 'foo', new ApiResultTestSerializableObject(
-                               new ApiResultTestStringifiableObject()
-                       ) );
-                       $this->fail( 'Expected exception not thrown' );
-               } catch ( UnexpectedValueException $ex ) {
-                       $this->assertSame(
-                               'ApiResultTestSerializableObject::serializeForApiResult() ' .
-                                       'returned an object of class ApiResultTestStringifiableObject',
-                               $ex->getMessage(),
-                               'Expected exception'
-                       );
-               }
-
-               try {
-                       $arr = [];
-                       ApiResult::setValue( $arr, 'foo', new ApiResultTestSerializableObject( NAN ) );
-                       $this->fail( 'Expected exception not thrown' );
-               } catch ( UnexpectedValueException $ex ) {
-                       $this->assertSame(
-                               'ApiResultTestSerializableObject::serializeForApiResult() ' .
-                                       'returned an invalid value: Cannot add non-finite floats to ApiResult',
-                               $ex->getMessage(),
-                               'Expected exception'
-                       );
-               }
-
-               $arr = [];
-               ApiResult::setValue( $arr, 'foo', new ApiResultTestSerializableObject(
-                       [
-                               'one' => new ApiResultTestStringifiableObject( '1' ),
-                               'two' => new ApiResultTestSerializableObject( 2 ),
-                       ]
-               ) );
-               $this->assertSame( [
-                       'one' => '1',
-                       'two' => 2,
-               ], $arr['foo'] );
-       }
-}
-
-class ApiResultTestStringifiableObject {
-       private $ret;
-
-       public function __construct( $ret = 'Ok' ) {
-               $this->ret = $ret;
-       }
-
-       public function __toString() {
-               return $this->ret;
-       }
-}
-
-class ApiResultTestSerializableObject {
-       private $ret;
-
-       public function __construct( $ret ) {
-               $this->ret = $ret;
-       }
-
-       public function __toString() {
-               return "Fail";
-       }
-
-       public function serializeForApiResult() {
-               return $this->ret;
-       }
-}
diff --git a/tests/phpunit/includes/api/ApiUsageExceptionTest.php b/tests/phpunit/includes/api/ApiUsageExceptionTest.php
deleted file mode 100644 (file)
index bb72021..0000000
+++ /dev/null
@@ -1,44 +0,0 @@
-<?php
-
-/**
- * @covers ApiUsageException
- */
-class ApiUsageExceptionTest extends MediaWikiTestCase {
-
-       public function testCreateWithStatusValue_CanGetAMessageObject() {
-               $messageKey = 'some-message-key';
-               $messageParameter = 'some-parameter';
-               $statusValue = new StatusValue();
-               $statusValue->fatal( $messageKey, $messageParameter );
-
-               $apiUsageException = new ApiUsageException( null, $statusValue );
-               /** @var \Message $gotMessage */
-               $gotMessage = $apiUsageException->getMessageObject();
-
-               $this->assertInstanceOf( \Message::class, $gotMessage );
-               $this->assertEquals( $messageKey, $gotMessage->getKey() );
-               $this->assertEquals( [ $messageParameter ], $gotMessage->getParams() );
-       }
-
-       public function testNewWithMessage_ThenGetMessageObject_ReturnsApiMessageWithProvidedData() {
-               $expectedMessage = new Message( 'some-message-key', [ 'some message parameter' ] );
-               $expectedCode = 'some-error-code';
-               $expectedData = [ 'some-error-data' ];
-
-               $apiUsageException = ApiUsageException::newWithMessage(
-                       null,
-                       $expectedMessage,
-                       $expectedCode,
-                       $expectedData
-               );
-               /** @var \ApiMessage $gotMessage */
-               $gotMessage = $apiUsageException->getMessageObject();
-
-               $this->assertInstanceOf( \ApiMessage::class, $gotMessage );
-               $this->assertEquals( $expectedMessage->getKey(), $gotMessage->getKey() );
-               $this->assertEquals( $expectedMessage->getParams(), $gotMessage->getParams() );
-               $this->assertEquals( $expectedCode, $gotMessage->getApiCode() );
-               $this->assertEquals( $expectedData, $gotMessage->getApiData() );
-       }
-
-}
diff --git a/tests/phpunit/includes/auth/AbstractPreAuthenticationProviderTest.php b/tests/phpunit/includes/auth/AbstractPreAuthenticationProviderTest.php
deleted file mode 100644 (file)
index 2970a28..0000000
+++ /dev/null
@@ -1,45 +0,0 @@
-<?php
-
-namespace MediaWiki\Auth;
-
-/**
- * @group AuthManager
- * @covers \MediaWiki\Auth\AbstractPreAuthenticationProvider
- */
-class AbstractPreAuthenticationProviderTest extends \MediaWikiTestCase {
-       public function testAbstractPreAuthenticationProvider() {
-               $user = \User::newFromName( 'UTSysop' );
-
-               $provider = $this->getMockForAbstractClass( AbstractPreAuthenticationProvider::class );
-
-               $this->assertEquals(
-                       [],
-                       $provider->getAuthenticationRequests( AuthManager::ACTION_LOGIN, [] )
-               );
-               $this->assertEquals(
-                       \StatusValue::newGood(),
-                       $provider->testForAuthentication( [] )
-               );
-               $this->assertEquals(
-                       \StatusValue::newGood(),
-                       $provider->testForAccountCreation( $user, $user, [] )
-               );
-               $this->assertEquals(
-                       \StatusValue::newGood(),
-                       $provider->testUserForCreation( $user, AuthManager::AUTOCREATE_SOURCE_SESSION )
-               );
-               $this->assertEquals(
-                       \StatusValue::newGood(),
-                       $provider->testUserForCreation( $user, false )
-               );
-               $this->assertEquals(
-                       \StatusValue::newGood(),
-                       $provider->testForAccountLink( $user )
-               );
-
-               $res = AuthenticationResponse::newPass();
-               $provider->postAuthentication( $user, $res );
-               $provider->postAccountCreation( $user, $user, $res );
-               $provider->postAccountLink( $user, $res );
-       }
-}
diff --git a/tests/phpunit/includes/auth/AbstractSecondaryAuthenticationProviderTest.php b/tests/phpunit/includes/auth/AbstractSecondaryAuthenticationProviderTest.php
deleted file mode 100644 (file)
index cd17862..0000000
+++ /dev/null
@@ -1,84 +0,0 @@
-<?php
-
-namespace MediaWiki\Auth;
-
-/**
- * @group AuthManager
- * @covers \MediaWiki\Auth\AbstractSecondaryAuthenticationProvider
- */
-class AbstractSecondaryAuthenticationProviderTest extends \MediaWikiTestCase {
-       public function testAbstractSecondaryAuthenticationProvider() {
-               $user = \User::newFromName( 'UTSysop' );
-
-               $provider = $this->getMockForAbstractClass( AbstractSecondaryAuthenticationProvider::class );
-
-               try {
-                       $provider->continueSecondaryAuthentication( $user, [] );
-                       $this->fail( 'Expected exception not thrown' );
-               } catch ( \BadMethodCallException $ex ) {
-               }
-
-               try {
-                       $provider->continueSecondaryAccountCreation( $user, $user, [] );
-                       $this->fail( 'Expected exception not thrown' );
-               } catch ( \BadMethodCallException $ex ) {
-               }
-
-               $req = $this->getMockForAbstractClass( AuthenticationRequest::class );
-
-               $this->assertTrue( $provider->providerAllowsPropertyChange( 'foo' ) );
-               $this->assertEquals(
-                       \StatusValue::newGood( 'ignored' ),
-                       $provider->providerAllowsAuthenticationDataChange( $req )
-               );
-               $this->assertEquals(
-                       \StatusValue::newGood(),
-                       $provider->testForAccountCreation( $user, $user, [] )
-               );
-               $this->assertEquals(
-                       \StatusValue::newGood(),
-                       $provider->testUserForCreation( $user, AuthManager::AUTOCREATE_SOURCE_SESSION )
-               );
-               $this->assertEquals(
-                       \StatusValue::newGood(),
-                       $provider->testUserForCreation( $user, false )
-               );
-
-               $provider->providerChangeAuthenticationData( $req );
-               $provider->autoCreatedAccount( $user, AuthManager::AUTOCREATE_SOURCE_SESSION );
-
-               $res = AuthenticationResponse::newPass();
-               $provider->postAuthentication( $user, $res );
-               $provider->postAccountCreation( $user, $user, $res );
-       }
-
-       public function testProviderRevokeAccessForUser() {
-               $reqs = [];
-               for ( $i = 0; $i < 3; $i++ ) {
-                       $reqs[$i] = $this->createMock( AuthenticationRequest::class );
-                       $reqs[$i]->done = false;
-               }
-
-               $provider = $this->getMockBuilder( AbstractSecondaryAuthenticationProvider::class )
-                       ->setMethods( [ 'providerChangeAuthenticationData' ] )
-                       ->getMockForAbstractClass();
-               $provider->expects( $this->once() )->method( 'getAuthenticationRequests' )
-                       ->with(
-                               $this->identicalTo( AuthManager::ACTION_REMOVE ),
-                               $this->identicalTo( [ 'username' => 'UTSysop' ] )
-                       )
-                       ->will( $this->returnValue( $reqs ) );
-               $provider->expects( $this->exactly( 3 ) )->method( 'providerChangeAuthenticationData' )
-                       ->will( $this->returnCallback( function ( $req ) {
-                               $this->assertSame( 'UTSysop', $req->username );
-                               $this->assertFalse( $req->done );
-                               $req->done = true;
-                       } ) );
-
-               $provider->providerRevokeAccessForUser( 'UTSysop' );
-
-               foreach ( $reqs as $i => $req ) {
-                       $this->assertTrue( $req->done, "#$i" );
-               }
-       }
-}
diff --git a/tests/phpunit/includes/auth/AuthenticationResponseTest.php b/tests/phpunit/includes/auth/AuthenticationResponseTest.php
deleted file mode 100644 (file)
index c796822..0000000
+++ /dev/null
@@ -1,112 +0,0 @@
-<?php
-
-namespace MediaWiki\Auth;
-
-/**
- * @group AuthManager
- * @covers \MediaWiki\Auth\AuthenticationResponse
- */
-class AuthenticationResponseTest extends \MediaWikiTestCase {
-       /**
-        * @dataProvider provideConstructors
-        * @param string $constructor
-        * @param array $args
-        * @param array|Exception $expect
-        */
-       public function testConstructors( $constructor, $args, $expect ) {
-               if ( is_array( $expect ) ) {
-                       $res = new AuthenticationResponse();
-                       $res->messageType = 'warning';
-                       foreach ( $expect as $field => $value ) {
-                               $res->$field = $value;
-                       }
-                       $ret = call_user_func_array( "MediaWiki\\Auth\\AuthenticationResponse::$constructor", $args );
-                       $this->assertEquals( $res, $ret );
-               } else {
-                       try {
-                               call_user_func_array( "MediaWiki\\Auth\\AuthenticationResponse::$constructor", $args );
-                               $this->fail( 'Expected exception not thrown' );
-                       } catch ( \Exception $ex ) {
-                               $this->assertEquals( $expect, $ex );
-                       }
-               }
-       }
-
-       public function provideConstructors() {
-               $req = $this->getMockForAbstractClass( AuthenticationRequest::class );
-               $msg = new \Message( 'mainpage' );
-
-               return [
-                       [ 'newPass', [], [
-                               'status' => AuthenticationResponse::PASS,
-                       ] ],
-                       [ 'newPass', [ 'name' ], [
-                               'status' => AuthenticationResponse::PASS,
-                               'username' => 'name',
-                       ] ],
-                       [ 'newPass', [ 'name', null ], [
-                               'status' => AuthenticationResponse::PASS,
-                               'username' => 'name',
-                       ] ],
-
-                       [ 'newFail', [ $msg ], [
-                               'status' => AuthenticationResponse::FAIL,
-                               'message' => $msg,
-                               'messageType' => 'error',
-                       ] ],
-
-                       [ 'newRestart', [ $msg ], [
-                               'status' => AuthenticationResponse::RESTART,
-                               'message' => $msg,
-                       ] ],
-
-                       [ 'newAbstain', [], [
-                               'status' => AuthenticationResponse::ABSTAIN,
-                       ] ],
-
-                       [ 'newUI', [ [ $req ], $msg ], [
-                               'status' => AuthenticationResponse::UI,
-                               'neededRequests' => [ $req ],
-                               'message' => $msg,
-                               'messageType' => 'warning',
-                       ] ],
-
-                       [ 'newUI', [ [ $req ], $msg, 'warning' ], [
-                               'status' => AuthenticationResponse::UI,
-                               'neededRequests' => [ $req ],
-                               'message' => $msg,
-                               'messageType' => 'warning',
-                       ] ],
-
-                       [ 'newUI', [ [ $req ], $msg, 'error' ], [
-                               'status' => AuthenticationResponse::UI,
-                               'neededRequests' => [ $req ],
-                               'message' => $msg,
-                               'messageType' => 'error',
-                       ] ],
-                       [ 'newUI', [ [], $msg ],
-                               new \InvalidArgumentException( '$reqs may not be empty' )
-                       ],
-
-                       [ 'newRedirect', [ [ $req ], 'http://example.org/redir' ], [
-                               'status' => AuthenticationResponse::REDIRECT,
-                               'neededRequests' => [ $req ],
-                               'redirectTarget' => 'http://example.org/redir',
-                       ] ],
-                       [
-                               'newRedirect',
-                               [ [ $req ], 'http://example.org/redir', [ 'foo' => 'bar' ] ],
-                               [
-                                       'status' => AuthenticationResponse::REDIRECT,
-                                       'neededRequests' => [ $req ],
-                                       'redirectTarget' => 'http://example.org/redir',
-                                       'redirectApiData' => [ 'foo' => 'bar' ],
-                               ]
-                       ],
-                       [ 'newRedirect', [ [], 'http://example.org/redir' ],
-                               new \InvalidArgumentException( '$reqs may not be empty' )
-                       ],
-               ];
-       }
-
-}
diff --git a/tests/phpunit/includes/auth/ConfirmLinkSecondaryAuthenticationProviderTest.php b/tests/phpunit/includes/auth/ConfirmLinkSecondaryAuthenticationProviderTest.php
deleted file mode 100644 (file)
index b17da2e..0000000
+++ /dev/null
@@ -1,289 +0,0 @@
-<?php
-
-namespace MediaWiki\Auth;
-
-use Wikimedia\TestingAccessWrapper;
-
-/**
- * @group AuthManager
- * @covers \MediaWiki\Auth\ConfirmLinkSecondaryAuthenticationProvider
- */
-class ConfirmLinkSecondaryAuthenticationProviderTest extends \MediaWikiTestCase {
-       /**
-        * @dataProvider provideGetAuthenticationRequests
-        * @param string $action
-        * @param array $response
-        */
-       public function testGetAuthenticationRequests( $action, $response ) {
-               $provider = new ConfirmLinkSecondaryAuthenticationProvider();
-
-               $this->assertEquals( $response, $provider->getAuthenticationRequests( $action, [] ) );
-       }
-
-       public static function provideGetAuthenticationRequests() {
-               return [
-                       [ AuthManager::ACTION_LOGIN, [] ],
-                       [ AuthManager::ACTION_CREATE, [] ],
-                       [ AuthManager::ACTION_LINK, [] ],
-                       [ AuthManager::ACTION_CHANGE, [] ],
-                       [ AuthManager::ACTION_REMOVE, [] ],
-               ];
-       }
-
-       public function testBeginSecondaryAuthentication() {
-               $user = \User::newFromName( 'UTSysop' );
-               $obj = new \stdClass;
-
-               $mock = $this->getMockBuilder( ConfirmLinkSecondaryAuthenticationProvider::class )
-                       ->setMethods( [ 'beginLinkAttempt', 'continueLinkAttempt' ] )
-                       ->getMock();
-               $mock->expects( $this->once() )->method( 'beginLinkAttempt' )
-                       ->with( $this->identicalTo( $user ), $this->identicalTo( 'AuthManager::authnState' ) )
-                       ->will( $this->returnValue( $obj ) );
-               $mock->expects( $this->never() )->method( 'continueLinkAttempt' );
-
-               $this->assertSame( $obj, $mock->beginSecondaryAuthentication( $user, [] ) );
-       }
-
-       public function testContinueSecondaryAuthentication() {
-               $user = \User::newFromName( 'UTSysop' );
-               $obj = new \stdClass;
-               $reqs = [ new \stdClass ];
-
-               $mock = $this->getMockBuilder( ConfirmLinkSecondaryAuthenticationProvider::class )
-                       ->setMethods( [ 'beginLinkAttempt', 'continueLinkAttempt' ] )
-                       ->getMock();
-               $mock->expects( $this->never() )->method( 'beginLinkAttempt' );
-               $mock->expects( $this->once() )->method( 'continueLinkAttempt' )
-                       ->with(
-                               $this->identicalTo( $user ),
-                               $this->identicalTo( 'AuthManager::authnState' ),
-                               $this->identicalTo( $reqs )
-                       )
-                       ->will( $this->returnValue( $obj ) );
-
-               $this->assertSame( $obj, $mock->continueSecondaryAuthentication( $user, $reqs ) );
-       }
-
-       public function testBeginSecondaryAccountCreation() {
-               $user = \User::newFromName( 'UTSysop' );
-               $obj = new \stdClass;
-
-               $mock = $this->getMockBuilder( ConfirmLinkSecondaryAuthenticationProvider::class )
-                       ->setMethods( [ 'beginLinkAttempt', 'continueLinkAttempt' ] )
-                       ->getMock();
-               $mock->expects( $this->once() )->method( 'beginLinkAttempt' )
-                       ->with( $this->identicalTo( $user ), $this->identicalTo( 'AuthManager::accountCreationState' ) )
-                       ->will( $this->returnValue( $obj ) );
-               $mock->expects( $this->never() )->method( 'continueLinkAttempt' );
-
-               $this->assertSame( $obj, $mock->beginSecondaryAccountCreation( $user, $user, [] ) );
-       }
-
-       public function testContinueSecondaryAccountCreation() {
-               $user = \User::newFromName( 'UTSysop' );
-               $obj = new \stdClass;
-               $reqs = [ new \stdClass ];
-
-               $mock = $this->getMockBuilder( ConfirmLinkSecondaryAuthenticationProvider::class )
-                       ->setMethods( [ 'beginLinkAttempt', 'continueLinkAttempt' ] )
-                       ->getMock();
-               $mock->expects( $this->never() )->method( 'beginLinkAttempt' );
-               $mock->expects( $this->once() )->method( 'continueLinkAttempt' )
-                       ->with(
-                               $this->identicalTo( $user ),
-                               $this->identicalTo( 'AuthManager::accountCreationState' ),
-                               $this->identicalTo( $reqs )
-                       )
-                       ->will( $this->returnValue( $obj ) );
-
-               $this->assertSame( $obj, $mock->continueSecondaryAccountCreation( $user, $user, $reqs ) );
-       }
-
-       /**
-        * Get requests for testing
-        * @return AuthenticationRequest[]
-        */
-       private function getLinkRequests() {
-               $reqs = [];
-
-               $mb = $this->getMockBuilder( AuthenticationRequest::class )
-                       ->setMethods( [ 'getUniqueId' ] );
-               for ( $i = 1; $i <= 3; $i++ ) {
-                       $req = $mb->getMockForAbstractClass();
-                       $req->expects( $this->any() )->method( 'getUniqueId' )
-                               ->will( $this->returnValue( "Request$i" ) );
-                       $req->id = $i - 1;
-                       $reqs[$req->getUniqueId()] = $req;
-               }
-
-               return $reqs;
-       }
-
-       public function testBeginLinkAttempt() {
-               $badReq = $this->getMockBuilder( AuthenticationRequest::class )
-                       ->setMethods( [ 'getUniqueId' ] )
-                       ->getMockForAbstractClass();
-               $badReq->expects( $this->any() )->method( 'getUniqueId' )
-                       ->will( $this->returnValue( "BadReq" ) );
-
-               $user = \User::newFromName( 'UTSysop' );
-               $provider = TestingAccessWrapper::newFromObject(
-                       new ConfirmLinkSecondaryAuthenticationProvider
-               );
-               $request = new \FauxRequest();
-               $manager = $this->getMockBuilder( AuthManager::class )
-                       ->setMethods( [ 'allowsAuthenticationDataChange' ] )
-                       ->setConstructorArgs( [ $request, \RequestContext::getMain()->getConfig() ] )
-                       ->getMock();
-               $manager->expects( $this->any() )->method( 'allowsAuthenticationDataChange' )
-                       ->will( $this->returnCallback( function ( $req ) {
-                               return $req->getUniqueId() !== 'BadReq'
-                                       ? \StatusValue::newGood()
-                                       : \StatusValue::newFatal( 'no' );
-                       } ) );
-               $provider->setManager( $manager );
-
-               $this->assertEquals(
-                       AuthenticationResponse::newAbstain(),
-                       $provider->beginLinkAttempt( $user, 'state' )
-               );
-
-               $request->getSession()->setSecret( 'state', [
-                       'maybeLink' => [],
-               ] );
-               $this->assertEquals(
-                       AuthenticationResponse::newAbstain(),
-                       $provider->beginLinkAttempt( $user, 'state' )
-               );
-
-               $reqs = $this->getLinkRequests();
-               $request->getSession()->setSecret( 'state', [
-                       'maybeLink' => $reqs + [ 'BadReq' => $badReq ]
-               ] );
-               $res = $provider->beginLinkAttempt( $user, 'state' );
-               $this->assertInstanceOf( AuthenticationResponse::class, $res );
-               $this->assertSame( AuthenticationResponse::UI, $res->status );
-               $this->assertSame( 'authprovider-confirmlink-message', $res->message->getKey() );
-               $this->assertCount( 1, $res->neededRequests );
-               $req = $res->neededRequests[0];
-               $this->assertInstanceOf( ConfirmLinkAuthenticationRequest::class, $req );
-               $expectReqs = $this->getLinkRequests();
-               foreach ( $expectReqs as $r ) {
-                       $r->action = AuthManager::ACTION_CHANGE;
-                       $r->username = $user->getName();
-               }
-               $this->assertEquals( $expectReqs, TestingAccessWrapper::newFromObject( $req )->linkRequests );
-       }
-
-       public function testContinueLinkAttempt() {
-               $user = \User::newFromName( 'UTSysop' );
-               $obj = new \stdClass;
-               $reqs = $this->getLinkRequests();
-
-               $done = [ false, false, false ];
-
-               // First, test the pass-through for not containing the ConfirmLinkAuthenticationRequest
-               $mock = $this->getMockBuilder( ConfirmLinkSecondaryAuthenticationProvider::class )
-                       ->setMethods( [ 'beginLinkAttempt' ] )
-                       ->getMock();
-               $mock->expects( $this->once() )->method( 'beginLinkAttempt' )
-                       ->with( $this->identicalTo( $user ), $this->identicalTo( 'state' ) )
-                       ->will( $this->returnValue( $obj ) );
-               $this->assertSame(
-                       $obj,
-                       TestingAccessWrapper::newFromObject( $mock )->continueLinkAttempt( $user, 'state', $reqs )
-               );
-
-               // Now test the actual functioning
-               $provider = $this->getMockBuilder( ConfirmLinkSecondaryAuthenticationProvider::class )
-                       ->setMethods( [
-                               'beginLinkAttempt', 'providerAllowsAuthenticationDataChange',
-                               'providerChangeAuthenticationData'
-                       ] )
-                       ->getMock();
-               $provider->expects( $this->never() )->method( 'beginLinkAttempt' );
-               $provider->expects( $this->any() )->method( 'providerAllowsAuthenticationDataChange' )
-                       ->will( $this->returnCallback( function ( $req ) use ( $reqs ) {
-                               return $req->getUniqueId() === 'Request3'
-                                       ? \StatusValue::newFatal( 'foo' ) : \StatusValue::newGood();
-                       } ) );
-               $provider->expects( $this->any() )->method( 'providerChangeAuthenticationData' )
-                       ->will( $this->returnCallback( function ( $req ) use ( &$done ) {
-                               $done[$req->id] = true;
-                       } ) );
-               $config = new \HashConfig( [
-                       'AuthManagerConfig' => [
-                               'preauth' => [],
-                               'primaryauth' => [],
-                               'secondaryauth' => [
-                                       [ 'factory' => function () use ( $provider ) {
-                                               return $provider;
-                                       } ],
-                               ],
-                       ],
-               ] );
-               $request = new \FauxRequest();
-               $manager = new AuthManager( $request, $config );
-               $provider->setManager( $manager );
-               $provider = TestingAccessWrapper::newFromObject( $provider );
-
-               $req = new ConfirmLinkAuthenticationRequest( $reqs );
-
-               $this->assertEquals(
-                       AuthenticationResponse::newAbstain(),
-                       $provider->continueLinkAttempt( $user, 'state', [ $req ] )
-               );
-
-               $request->getSession()->setSecret( 'state', [
-                       'maybeLink' => [],
-               ] );
-               $this->assertEquals(
-                       AuthenticationResponse::newAbstain(),
-                       $provider->continueLinkAttempt( $user, 'state', [ $req ] )
-               );
-
-               $request->getSession()->setSecret( 'state', [
-                       'maybeLink' => $reqs
-               ] );
-               $this->assertEquals(
-                       AuthenticationResponse::newPass(),
-                       $res = $provider->continueLinkAttempt( $user, 'state', [ $req ] )
-               );
-               $this->assertSame( [ false, false, false ], $done );
-
-               $request->getSession()->setSecret( 'state', [
-                       'maybeLink' => [ $reqs['Request2'] ],
-               ] );
-               $req->confirmedLinkIDs = [ 'Request1', 'Request2' ];
-               $res = $provider->continueLinkAttempt( $user, 'state', [ $req ] );
-               $this->assertEquals( AuthenticationResponse::newPass(), $res );
-               $this->assertSame( [ false, true, false ], $done );
-               $done = [ false, false, false ];
-
-               $request->getSession()->setSecret( 'state', [
-                       'maybeLink' => $reqs,
-               ] );
-               $req->confirmedLinkIDs = [ 'Request1', 'Request2' ];
-               $res = $provider->continueLinkAttempt( $user, 'state', [ $req ] );
-               $this->assertEquals( AuthenticationResponse::newPass(), $res );
-               $this->assertSame( [ true, true, false ], $done );
-               $done = [ false, false, false ];
-
-               $request->getSession()->setSecret( 'state', [
-                       'maybeLink' => $reqs,
-               ] );
-               $req->confirmedLinkIDs = [ 'Request1', 'Request3' ];
-               $res = $provider->continueLinkAttempt( $user, 'state', [ $req ] );
-               $this->assertEquals( AuthenticationResponse::UI, $res->status );
-               $this->assertCount( 1, $res->neededRequests );
-               $this->assertInstanceOf( ButtonAuthenticationRequest::class, $res->neededRequests[0] );
-               $this->assertSame( [ true, false, false ], $done );
-               $done = [ false, false, false ];
-
-               $res = $provider->continueLinkAttempt( $user, 'state', [ $res->neededRequests[0] ] );
-               $this->assertEquals( AuthenticationResponse::newPass(), $res );
-               $this->assertSame( [ false, false, false ], $done );
-       }
-
-}
diff --git a/tests/phpunit/includes/auth/EmailNotificationSecondaryAuthenticationProviderTest.php b/tests/phpunit/includes/auth/EmailNotificationSecondaryAuthenticationProviderTest.php
deleted file mode 100644 (file)
index ff22def..0000000
+++ /dev/null
@@ -1,112 +0,0 @@
-<?php
-
-namespace MediaWiki\Auth;
-
-use Psr\Log\LoggerInterface;
-use Wikimedia\TestingAccessWrapper;
-
-/**
- * @covers \MediaWiki\Auth\EmailNotificationSecondaryAuthenticationProvider
- */
-class EmailNotificationSecondaryAuthenticationProviderTest extends \PHPUnit\Framework\TestCase {
-       public function testConstructor() {
-               $config = new \HashConfig( [
-                       'EnableEmail' => true,
-                       'EmailAuthentication' => true,
-               ] );
-
-               $provider = new EmailNotificationSecondaryAuthenticationProvider();
-               $provider->setConfig( $config );
-               $providerPriv = TestingAccessWrapper::newFromObject( $provider );
-               $this->assertTrue( $providerPriv->sendConfirmationEmail );
-
-               $provider = new EmailNotificationSecondaryAuthenticationProvider( [
-                       'sendConfirmationEmail' => false,
-               ] );
-               $provider->setConfig( $config );
-               $providerPriv = TestingAccessWrapper::newFromObject( $provider );
-               $this->assertFalse( $providerPriv->sendConfirmationEmail );
-       }
-
-       /**
-        * @dataProvider provideGetAuthenticationRequests
-        * @param string $action
-        * @param AuthenticationRequest[] $expected
-        */
-       public function testGetAuthenticationRequests( $action, $expected ) {
-               $provider = new EmailNotificationSecondaryAuthenticationProvider( [
-                       'sendConfirmationEmail' => true,
-               ] );
-               $this->assertSame( $expected, $provider->getAuthenticationRequests( $action, [] ) );
-       }
-
-       public function provideGetAuthenticationRequests() {
-               return [
-                       [ AuthManager::ACTION_LOGIN, [] ],
-                       [ AuthManager::ACTION_CREATE, [] ],
-                       [ AuthManager::ACTION_LINK, [] ],
-                       [ AuthManager::ACTION_CHANGE, [] ],
-                       [ AuthManager::ACTION_REMOVE, [] ],
-               ];
-       }
-
-       public function testBeginSecondaryAuthentication() {
-               $provider = new EmailNotificationSecondaryAuthenticationProvider( [
-                       'sendConfirmationEmail' => true,
-               ] );
-               $this->assertEquals( AuthenticationResponse::newAbstain(),
-                       $provider->beginSecondaryAuthentication( \User::newFromName( 'Foo' ), [] ) );
-       }
-
-       public function testBeginSecondaryAccountCreation() {
-               $authManager = new AuthManager( new \FauxRequest(), new \HashConfig() );
-
-               $creator = $this->getMockBuilder( \User::class )->getMock();
-               $userWithoutEmail = $this->getMockBuilder( \User::class )->getMock();
-               $userWithoutEmail->expects( $this->any() )->method( 'getEmail' )->willReturn( '' );
-               $userWithoutEmail->expects( $this->any() )->method( 'getInstanceForUpdate' )->willReturnSelf();
-               $userWithoutEmail->expects( $this->never() )->method( 'sendConfirmationMail' );
-               $userWithEmailError = $this->getMockBuilder( \User::class )->getMock();
-               $userWithEmailError->expects( $this->any() )->method( 'getEmail' )->willReturn( 'foo@bar.baz' );
-               $userWithEmailError->expects( $this->any() )->method( 'getInstanceForUpdate' )->willReturnSelf();
-               $userWithEmailError->expects( $this->any() )->method( 'sendConfirmationMail' )
-                       ->willReturn( \Status::newFatal( 'fail' ) );
-               $userExpectsConfirmation = $this->getMockBuilder( \User::class )->getMock();
-               $userExpectsConfirmation->expects( $this->any() )->method( 'getEmail' )
-                       ->willReturn( 'foo@bar.baz' );
-               $userExpectsConfirmation->expects( $this->any() )->method( 'getInstanceForUpdate' )
-                       ->willReturnSelf();
-               $userExpectsConfirmation->expects( $this->once() )->method( 'sendConfirmationMail' )
-                       ->willReturn( \Status::newGood() );
-               $userNotExpectsConfirmation = $this->getMockBuilder( \User::class )->getMock();
-               $userNotExpectsConfirmation->expects( $this->any() )->method( 'getEmail' )
-                       ->willReturn( 'foo@bar.baz' );
-               $userNotExpectsConfirmation->expects( $this->any() )->method( 'getInstanceForUpdate' )
-                       ->willReturnSelf();
-               $userNotExpectsConfirmation->expects( $this->never() )->method( 'sendConfirmationMail' );
-
-               $provider = new EmailNotificationSecondaryAuthenticationProvider( [
-                       'sendConfirmationEmail' => false,
-               ] );
-               $provider->setManager( $authManager );
-               $provider->beginSecondaryAccountCreation( $userNotExpectsConfirmation, $creator, [] );
-
-               $provider = new EmailNotificationSecondaryAuthenticationProvider( [
-                       'sendConfirmationEmail' => true,
-               ] );
-               $provider->setManager( $authManager );
-               $provider->beginSecondaryAccountCreation( $userWithoutEmail, $creator, [] );
-               $provider->beginSecondaryAccountCreation( $userExpectsConfirmation, $creator, [] );
-
-               // test logging of email errors
-               $logger = $this->getMockForAbstractClass( LoggerInterface::class );
-               $logger->expects( $this->once() )->method( 'warning' );
-               $provider->setLogger( $logger );
-               $provider->beginSecondaryAccountCreation( $userWithEmailError, $creator, [] );
-
-               // test disable flag used by other providers
-               $authManager->setAuthenticationSessionData( 'no-email', true );
-               $provider->setManager( $authManager );
-               $provider->beginSecondaryAccountCreation( $userNotExpectsConfirmation, $creator, [] );
-       }
-}
diff --git a/tests/phpunit/includes/changes/ChangesListFilterGroupTest.php b/tests/phpunit/includes/changes/ChangesListFilterGroupTest.php
deleted file mode 100644 (file)
index 6190516..0000000
+++ /dev/null
@@ -1,79 +0,0 @@
-<?php
-
-/**
- * @covers ChangesListFilterGroup
- */
-class ChangesListFilterGroupTest extends MediaWikiTestCase {
-       /**
-        * phpcs:disable Generic.Files.LineLength
-        * @expectedException MWException
-        * @expectedExceptionMessage Group names may not contain '_'.  Use the naming convention: 'camelCase'
-        * phpcs:enable
-        */
-       public function testReservedCharacter() {
-               new MockChangesListFilterGroup(
-                       [
-                               'type' => 'some_type',
-                               'name' => 'group_name',
-                               'priority' => 1,
-                               'filters' => [],
-                       ]
-               );
-       }
-
-       public function testAutoPriorities() {
-               $group = new MockChangesListFilterGroup(
-                       [
-                               'type' => 'some_type',
-                               'name' => 'groupName',
-                               'isFullCoverage' => true,
-                               'priority' => 1,
-                               'filters' => [
-                                       [ 'name' => 'hidefoo' ],
-                                       [ 'name' => 'hidebar' ],
-                                       [ 'name' => 'hidebaz' ],
-                               ],
-                       ]
-               );
-
-               $filters = $group->getFilters();
-               $this->assertEquals(
-                       [
-                               -2,
-                               -3,
-                               -4,
-                       ],
-                       array_map(
-                               function ( $f ) {
-                                       return $f->getPriority();
-                               },
-                               array_values( $filters )
-                       )
-               );
-       }
-
-       // Get without warnings
-       public function testGetFilter() {
-               $group = new MockChangesListFilterGroup(
-                       [
-                               'type' => 'some_type',
-                               'name' => 'groupName',
-                               'isFullCoverage' => true,
-                               'priority' => 1,
-                               'filters' => [
-                                       [ 'name' => 'foo' ],
-                               ],
-                       ]
-               );
-
-               $this->assertEquals(
-                       'foo',
-                       $group->getFilter( 'foo' )->getName()
-               );
-
-               $this->assertEquals(
-                       null,
-                       $group->getFilter( 'bar' )
-               );
-       }
-}
diff --git a/tests/phpunit/includes/collation/CustomUppercaseCollationTest.php b/tests/phpunit/includes/collation/CustomUppercaseCollationTest.php
deleted file mode 100644 (file)
index 417b468..0000000
+++ /dev/null
@@ -1,68 +0,0 @@
-<?php
-
-/**
- * @covers CustomUppercaseCollation
- */
-class CustomUppercaseCollationTest extends MediaWikiTestCase {
-
-       public function setUp() {
-               $this->collation = new CustomUppercaseCollation( [
-                       'D',
-                       'C',
-                       'Cs',
-                       'B'
-               ], Language::factory( 'en' ) );
-
-               parent::setUp();
-       }
-
-       /**
-        * @dataProvider providerOrder
-        */
-       public function testOrder( $first, $second, $msg ) {
-               $sortkey1 = $this->collation->getSortKey( $first );
-               $sortkey2 = $this->collation->getSortKey( $second );
-
-               $this->assertTrue( strcmp( $sortkey1, $sortkey2 ) < 0, $msg );
-       }
-
-       public function providerOrder() {
-               return [
-                       [ 'X', 'Z', 'Maintain order of unrearranged' ],
-                       [ 'D', 'C', 'Actually resorts' ],
-                       [ 'D', 'B', 'resort test 2' ],
-                       [ 'Adobe', 'Abode', 'not first letter' ],
-                       [ '💩 ', 'C', 'Test relocated to end' ],
-                       [ 'c', 'b', 'lowercase' ],
-                       [ 'x', 'z', 'lowercase original' ],
-                       [ 'Cz', 'Cs', 'digraphs' ],
-                       [ 'C50D', 'C100', 'Numbers' ]
-               ];
-       }
-
-       /**
-        * @dataProvider provideGetFirstLetter
-        */
-       public function testGetFirstLetter( $string, $first ) {
-               $this->assertSame( $this->collation->getFirstLetter( $string ), $first );
-       }
-
-       public function provideGetFirstLetter() {
-               return [
-                       [ 'Do', 'D' ],
-                       [ 'do', 'D' ],
-                       [ 'Ao', 'A' ],
-                       [ 'afdsa', 'A' ],
-                       [ "\u{F3000}Foo", 'D' ],
-                       [ "\u{F3001}Foo", 'C' ],
-                       [ "\u{F3002}Foo", 'Cs' ],
-                       [ "\u{F3003}Foo", 'B' ],
-                       [ "\u{F3004}Foo", "\u{F3004}" ],
-                       [ 'C', 'C' ],
-                       [ 'Cz', 'C' ],
-                       [ 'Cs', 'Cs' ],
-                       [ 'CS', 'Cs' ],
-                       [ 'cs', 'Cs' ],
-               ];
-       }
-}
diff --git a/tests/phpunit/includes/composer/ComposerVersionNormalizerTest.php b/tests/phpunit/includes/composer/ComposerVersionNormalizerTest.php
deleted file mode 100644 (file)
index c5c0dc7..0000000
+++ /dev/null
@@ -1,163 +0,0 @@
-<?php
-
-/**
- * @covers ComposerVersionNormalizer
- *
- * @group ComposerHooks
- *
- * @author Jeroen De Dauw < jeroendedauw@gmail.com >
- */
-class ComposerVersionNormalizerTest extends PHPUnit\Framework\TestCase {
-
-       use MediaWikiCoversValidator;
-       use PHPUnit4And6Compat;
-
-       /**
-        * @dataProvider nonStringProvider
-        */
-       public function testGivenNonString_normalizeThrowsInvalidArgumentException( $nonString ) {
-               $normalizer = new ComposerVersionNormalizer();
-
-               $this->setExpectedException( InvalidArgumentException::class );
-               $normalizer->normalizeSuffix( $nonString );
-       }
-
-       public function nonStringProvider() {
-               return [
-                       [ null ],
-                       [ 42 ],
-                       [ [] ],
-                       [ new stdClass() ],
-                       [ true ],
-               ];
-       }
-
-       /**
-        * @dataProvider simpleVersionProvider
-        */
-       public function testGivenSimpleVersion_normalizeSuffixReturnsAsIs( $simpleVersion ) {
-               $this->assertRemainsUnchanged( $simpleVersion );
-       }
-
-       protected function assertRemainsUnchanged( $version ) {
-               $normalizer = new ComposerVersionNormalizer();
-
-               $this->assertEquals(
-                       $version,
-                       $normalizer->normalizeSuffix( $version )
-               );
-       }
-
-       public function simpleVersionProvider() {
-               return [
-                       [ '1.22.0' ],
-                       [ '1.19.2' ],
-                       [ '1.19.2.0' ],
-                       [ '1.9' ],
-                       [ '123.321.456.654' ],
-               ];
-       }
-
-       /**
-        * @dataProvider complexVersionProvider
-        */
-       public function testGivenComplexVersionWithoutDash_normalizeSuffixAddsDash(
-               $withoutDash, $withDash
-       ) {
-               $normalizer = new ComposerVersionNormalizer();
-
-               $this->assertEquals(
-                       $withDash,
-                       $normalizer->normalizeSuffix( $withoutDash )
-               );
-       }
-
-       public function complexVersionProvider() {
-               return [
-                       [ '1.22.0alpha', '1.22.0-alpha' ],
-                       [ '1.22.0RC', '1.22.0-RC' ],
-                       [ '1.19beta', '1.19-beta' ],
-                       [ '1.9RC4', '1.9-RC4' ],
-                       [ '1.9.1.2RC4', '1.9.1.2-RC4' ],
-                       [ '1.9.1.2RC', '1.9.1.2-RC' ],
-                       [ '123.321.456.654RC9001', '123.321.456.654-RC9001' ],
-               ];
-       }
-
-       /**
-        * @dataProvider complexVersionProvider
-        */
-       public function testGivenComplexVersionWithDash_normalizeSuffixReturnsAsIs(
-               $withoutDash, $withDash
-       ) {
-               $this->assertRemainsUnchanged( $withDash );
-       }
-
-       /**
-        * @dataProvider fourLevelVersionsProvider
-        */
-       public function testGivenFourLevels_levelCountNormalizationDoesNothing( $version ) {
-               $normalizer = new ComposerVersionNormalizer();
-
-               $this->assertEquals(
-                       $version,
-                       $normalizer->normalizeLevelCount( $version )
-               );
-       }
-
-       public function fourLevelVersionsProvider() {
-               return [
-                       [ '1.22.0.0' ],
-                       [ '1.19.2.4' ],
-                       [ '1.19.2.0' ],
-                       [ '1.9.0.1' ],
-                       [ '123.321.456.654' ],
-                       [ '123.321.456.654RC4' ],
-                       [ '123.321.456.654-RC4' ],
-               ];
-       }
-
-       /**
-        * @dataProvider levelNormalizationProvider
-        */
-       public function testGivenFewerLevels_levelCountNormalizationEnsuresFourLevels(
-               $expected, $version
-       ) {
-               $normalizer = new ComposerVersionNormalizer();
-
-               $this->assertEquals(
-                       $expected,
-                       $normalizer->normalizeLevelCount( $version )
-               );
-       }
-
-       public function levelNormalizationProvider() {
-               return [
-                       [ '1.22.0.0', '1.22' ],
-                       [ '1.22.0.0', '1.22.0' ],
-                       [ '1.19.2.0', '1.19.2' ],
-                       [ '12345.0.0.0', '12345' ],
-                       [ '12345.0.0.0-RC4', '12345-RC4' ],
-                       [ '12345.0.0.0-alpha', '12345-alpha' ],
-               ];
-       }
-
-       /**
-        * @dataProvider invalidVersionProvider
-        */
-       public function testGivenInvalidVersion_normalizeSuffixReturnsAsIs( $invalidVersion ) {
-               $this->assertRemainsUnchanged( $invalidVersion );
-       }
-
-       public function invalidVersionProvider() {
-               return [
-                       [ '1.221-a' ],
-                       [ '1.221-' ],
-                       [ '1.22rc4a' ],
-                       [ 'a1.22rc' ],
-                       [ '.1.22rc' ],
-                       [ 'a' ],
-                       [ 'alpha42' ],
-               ];
-       }
-}
diff --git a/tests/phpunit/includes/config/ConfigFactoryTest.php b/tests/phpunit/includes/config/ConfigFactoryTest.php
deleted file mode 100644 (file)
index ea747af..0000000
+++ /dev/null
@@ -1,168 +0,0 @@
-<?php
-
-use MediaWiki\MediaWikiServices;
-
-class ConfigFactoryTest extends MediaWikiTestCase {
-
-       /**
-        * @covers ConfigFactory::register
-        */
-       public function testRegister() {
-               $factory = new ConfigFactory();
-               $factory->register( 'unittest', 'GlobalVarConfig::newInstance' );
-               $this->assertInstanceOf( GlobalVarConfig::class, $factory->makeConfig( 'unittest' ) );
-       }
-
-       /**
-        * @covers ConfigFactory::register
-        */
-       public function testRegisterInvalid() {
-               $factory = new ConfigFactory();
-               $this->setExpectedException( InvalidArgumentException::class );
-               $factory->register( 'invalid', 'Invalid callback' );
-       }
-
-       /**
-        * @covers ConfigFactory::register
-        */
-       public function testRegisterInvalidInstance() {
-               $factory = new ConfigFactory();
-               $this->setExpectedException( InvalidArgumentException::class );
-               $factory->register( 'invalidInstance', new stdClass );
-       }
-
-       /**
-        * @covers ConfigFactory::register
-        */
-       public function testRegisterInstance() {
-               $config = GlobalVarConfig::newInstance();
-               $factory = new ConfigFactory();
-               $factory->register( 'unittest', $config );
-               $this->assertSame( $config, $factory->makeConfig( 'unittest' ) );
-       }
-
-       /**
-        * @covers ConfigFactory::register
-        */
-       public function testRegisterAgain() {
-               $factory = new ConfigFactory();
-               $factory->register( 'unittest', 'GlobalVarConfig::newInstance' );
-               $config1 = $factory->makeConfig( 'unittest' );
-
-               $factory->register( 'unittest', 'GlobalVarConfig::newInstance' );
-               $config2 = $factory->makeConfig( 'unittest' );
-
-               $this->assertNotSame( $config1, $config2 );
-       }
-
-       /**
-        * @covers ConfigFactory::salvage
-        */
-       public function testSalvage() {
-               $oldFactory = new ConfigFactory();
-               $oldFactory->register( 'foo', 'GlobalVarConfig::newInstance' );
-               $oldFactory->register( 'bar', 'GlobalVarConfig::newInstance' );
-               $oldFactory->register( 'quux', 'GlobalVarConfig::newInstance' );
-
-               // instantiate two of the three defined configurations
-               $foo = $oldFactory->makeConfig( 'foo' );
-               $bar = $oldFactory->makeConfig( 'bar' );
-               $quux = $oldFactory->makeConfig( 'quux' );
-
-               // define new config instance
-               $newFactory = new ConfigFactory();
-               $newFactory->register( 'foo', 'GlobalVarConfig::newInstance' );
-               $newFactory->register( 'bar', function () {
-                       return new HashConfig();
-               } );
-
-               // "foo" and "quux" are defined in the old and the new factory.
-               // The old factory has instances for "foo" and "bar", but not "quux".
-               $newFactory->salvage( $oldFactory );
-
-               $newFoo = $newFactory->makeConfig( 'foo' );
-               $this->assertSame( $foo, $newFoo, 'existing instance should be salvaged' );
-
-               $newBar = $newFactory->makeConfig( 'bar' );
-               $this->assertNotSame( $bar, $newBar, 'don\'t salvage if callbacks differ' );
-
-               // the new factory doesn't have quux defined, so the quux instance should not be salvaged
-               $this->setExpectedException( ConfigException::class );
-               $newFactory->makeConfig( 'quux' );
-       }
-
-       /**
-        * @covers ConfigFactory::getConfigNames
-        */
-       public function testGetConfigNames() {
-               $factory = new ConfigFactory();
-               $factory->register( 'foo', 'GlobalVarConfig::newInstance' );
-               $factory->register( 'bar', new HashConfig() );
-
-               $this->assertEquals( [ 'foo', 'bar' ], $factory->getConfigNames() );
-       }
-
-       /**
-        * @covers ConfigFactory::makeConfig
-        */
-       public function testMakeConfigWithCallback() {
-               $factory = new ConfigFactory();
-               $factory->register( 'unittest', 'GlobalVarConfig::newInstance' );
-
-               $conf = $factory->makeConfig( 'unittest' );
-               $this->assertInstanceOf( Config::class, $conf );
-               $this->assertSame( $conf, $factory->makeConfig( 'unittest' ) );
-       }
-
-       /**
-        * @covers ConfigFactory::makeConfig
-        */
-       public function testMakeConfigWithObject() {
-               $factory = new ConfigFactory();
-               $conf = new HashConfig();
-               $factory->register( 'test', $conf );
-               $this->assertSame( $conf, $factory->makeConfig( 'test' ) );
-       }
-
-       /**
-        * @covers ConfigFactory::makeConfig
-        */
-       public function testMakeConfigFallback() {
-               $factory = new ConfigFactory();
-               $factory->register( '*', 'GlobalVarConfig::newInstance' );
-               $conf = $factory->makeConfig( 'unittest' );
-               $this->assertInstanceOf( Config::class, $conf );
-       }
-
-       /**
-        * @covers ConfigFactory::makeConfig
-        */
-       public function testMakeConfigWithNoBuilders() {
-               $factory = new ConfigFactory();
-               $this->setExpectedException( ConfigException::class );
-               $factory->makeConfig( 'nobuilderregistered' );
-       }
-
-       /**
-        * @covers ConfigFactory::makeConfig
-        */
-       public function testMakeConfigWithInvalidCallback() {
-               $factory = new ConfigFactory();
-               $factory->register( 'unittest', function () {
-                       return true; // Not a Config object
-               } );
-               $this->setExpectedException( UnexpectedValueException::class );
-               $factory->makeConfig( 'unittest' );
-       }
-
-       /**
-        * @covers ConfigFactory::getDefaultInstance
-        */
-       public function testGetDefaultInstance() {
-               // NOTE: the global config factory returned here has been overwritten
-               // for operation in test mode. It may not reflect LocalSettings.
-               $factory = MediaWikiServices::getInstance()->getConfigFactory();
-               $this->assertInstanceOf( Config::class, $factory->makeConfig( 'main' ) );
-       }
-
-}
diff --git a/tests/phpunit/includes/config/EtcdConfigTest.php b/tests/phpunit/includes/config/EtcdConfigTest.php
deleted file mode 100644 (file)
index 3eecf82..0000000
+++ /dev/null
@@ -1,621 +0,0 @@
-<?php
-
-use Wikimedia\TestingAccessWrapper;
-
-class EtcdConfigTest extends PHPUnit\Framework\TestCase {
-
-       use MediaWikiCoversValidator;
-       use PHPUnit4And6Compat;
-
-       private function createConfigMock( array $options = [] ) {
-               return $this->getMockBuilder( EtcdConfig::class )
-                       ->setConstructorArgs( [ $options + [
-                               'host' => 'etcd-tcp.example.net',
-                               'directory' => '/',
-                               'timeout' => 0.1,
-                       ] ] )
-                       ->setMethods( [ 'fetchAllFromEtcd' ] )
-                       ->getMock();
-       }
-
-       private static function createEtcdResponse( array $response ) {
-               $baseResponse = [
-                       'config' => null,
-                       'error' => null,
-                       'retry' => false,
-                       'modifiedIndex' => 0,
-               ];
-               return array_merge( $baseResponse, $response );
-       }
-
-       private function createSimpleConfigMock( array $config, $index = 0 ) {
-               $mock = $this->createConfigMock();
-               $mock->expects( $this->once() )->method( 'fetchAllFromEtcd' )
-                       ->willReturn( self::createEtcdResponse( [
-                               'config' => $config,
-                               'modifiedIndex' => $index,
-                       ] ) );
-               return $mock;
-       }
-
-       /**
-        * @covers EtcdConfig::has
-        */
-       public function testHasKnown() {
-               $config = $this->createSimpleConfigMock( [
-                       'known' => 'value'
-               ] );
-               $this->assertSame( true, $config->has( 'known' ) );
-       }
-
-       /**
-        * @covers EtcdConfig::__construct
-        * @covers EtcdConfig::get
-        */
-       public function testGetKnown() {
-               $config = $this->createSimpleConfigMock( [
-                       'known' => 'value'
-               ] );
-               $this->assertSame( 'value', $config->get( 'known' ) );
-       }
-
-       /**
-        * @covers EtcdConfig::has
-        */
-       public function testHasUnknown() {
-               $config = $this->createSimpleConfigMock( [
-                       'known' => 'value'
-               ] );
-               $this->assertSame( false, $config->has( 'unknown' ) );
-       }
-
-       /**
-        * @covers EtcdConfig::get
-        */
-       public function testGetUnknown() {
-               $config = $this->createSimpleConfigMock( [
-                       'known' => 'value'
-               ] );
-               $this->setExpectedException( ConfigException::class );
-               $config->get( 'unknown' );
-       }
-
-       /**
-        * @covers EtcdConfig::getModifiedIndex
-        */
-       public function testGetModifiedIndex() {
-               $config = $this->createSimpleConfigMock(
-                       [ 'some' => 'value' ],
-                       123
-               );
-               $this->assertSame( 123, $config->getModifiedIndex() );
-       }
-
-       /**
-        * @covers EtcdConfig::__construct
-        */
-       public function testConstructCacheObj() {
-               $cache = $this->getMockBuilder( HashBagOStuff::class )
-                       ->setMethods( [ 'get' ] )
-                       ->getMock();
-               $cache->expects( $this->once() )->method( 'get' )
-                       ->willReturn( [
-                               'config' => [ 'known' => 'from-cache' ],
-                               'expires' => INF,
-                               'modifiedIndex' => 123
-                       ] );
-               $config = $this->createConfigMock( [ 'cache' => $cache ] );
-
-               $this->assertSame( 'from-cache', $config->get( 'known' ) );
-       }
-
-       /**
-        * @covers EtcdConfig::__construct
-        */
-       public function testConstructCacheSpec() {
-               $config = $this->createConfigMock( [ 'cache' => [
-                       'class' => HashBagOStuff::class
-               ] ] );
-               $config->expects( $this->once() )->method( 'fetchAllFromEtcd' )
-                       ->willReturn( self::createEtcdResponse(
-                               [ 'config' => [ 'known' => 'from-fetch' ], ] ) );
-
-               $this->assertSame( 'from-fetch', $config->get( 'known' ) );
-       }
-
-       /**
-        * Test matrix
-        *
-        * - [x] Cache miss
-        *       Result: Fetched value
-        *       > cache miss | gets lock | backend succeeds
-        *
-        * - [x] Cache miss with backend error
-        *       Result: ConfigException
-        *       > cache miss | gets lock | backend error (no retry)
-        *
-        * - [x] Cache hit after retry
-        *       Result: Cached value (populated by process holding lock)
-        *       > cache miss | no lock | cache retry
-        *
-        * - [x] Cache hit
-        *       Result: Cached value
-        *       > cache hit
-        *
-        * - [x] Process cache hit
-        *       Result: Cached value
-        *       > process cache hit
-        *
-        * - [x] Cache expired
-        *       Result: Fetched value
-        *       > cache expired | gets lock | backend succeeds
-        *
-        * - [x] Cache expired with backend failure
-        *       Result: Cached value (stale)
-        *       > cache expired | gets lock | backend fails (allows retry)
-        *
-        * - [x] Cache expired and no lock
-        *       Result: Cached value (stale)
-        *       > cache expired | no lock
-        *
-        * Other notable scenarios:
-        *
-        * - [ ] Cache miss with backend retry
-        *       Result: Fetched value
-        *       > cache expired | gets lock | backend failure (allows retry)
-        */
-
-       /**
-        * @covers EtcdConfig::load
-        */
-       public function testLoadCacheMiss() {
-               // Create cache mock
-               $cache = $this->getMockBuilder( HashBagOStuff::class )
-                       ->setMethods( [ 'get', 'lock' ] )
-                       ->getMock();
-               // .. misses cache
-               $cache->expects( $this->once() )->method( 'get' )
-                       ->willReturn( false );
-               // .. gets lock
-               $cache->expects( $this->once() )->method( 'lock' )
-                       ->willReturn( true );
-
-               // Create config mock
-               $mock = $this->createConfigMock( [
-                       'cache' => $cache,
-               ] );
-               $mock->expects( $this->once() )->method( 'fetchAllFromEtcd' )
-                       ->willReturn(
-                               self::createEtcdResponse( [ 'config' => [ 'known' => 'from-fetch' ] ] ) );
-
-               $this->assertSame( 'from-fetch', $mock->get( 'known' ) );
-       }
-
-       /**
-        * @covers EtcdConfig::load
-        */
-       public function testLoadCacheMissBackendError() {
-               // Create cache mock
-               $cache = $this->getMockBuilder( HashBagOStuff::class )
-                       ->setMethods( [ 'get', 'lock' ] )
-                       ->getMock();
-               // .. misses cache
-               $cache->expects( $this->once() )->method( 'get' )
-                       ->willReturn( false );
-               // .. gets lock
-               $cache->expects( $this->once() )->method( 'lock' )
-                       ->willReturn( true );
-
-               // Create config mock
-               $mock = $this->createConfigMock( [
-                       'cache' => $cache,
-               ] );
-               $mock->expects( $this->once() )->method( 'fetchAllFromEtcd' )
-                       ->willReturn( self::createEtcdResponse( [ 'error' => 'Fake error', ] ) );
-
-               $this->setExpectedException( ConfigException::class );
-               $mock->get( 'key' );
-       }
-
-       /**
-        * @covers EtcdConfig::load
-        */
-       public function testLoadCacheMissWithoutLock() {
-               // Create cache mock
-               $cache = $this->getMockBuilder( HashBagOStuff::class )
-                       ->setMethods( [ 'get', 'lock' ] )
-                       ->getMock();
-               $cache->expects( $this->exactly( 2 ) )->method( 'get' )
-                       ->will( $this->onConsecutiveCalls(
-                               // .. misses cache first time
-                               false,
-                               // .. hits cache on retry
-                               [
-                                       'config' => [ 'known' => 'from-cache' ],
-                                       'expires' => INF,
-                                       'modifiedIndex' => 123
-                               ]
-                       ) );
-               // .. misses lock
-               $cache->expects( $this->once() )->method( 'lock' )
-                       ->willReturn( false );
-
-               // Create config mock
-               $mock = $this->createConfigMock( [
-                       'cache' => $cache,
-               ] );
-               $mock->expects( $this->never() )->method( 'fetchAllFromEtcd' );
-
-               $this->assertSame( 'from-cache', $mock->get( 'known' ) );
-       }
-
-       /**
-        * @covers EtcdConfig::load
-        */
-       public function testLoadCacheHit() {
-               // Create cache mock
-               $cache = $this->getMockBuilder( HashBagOStuff::class )
-                       ->setMethods( [ 'get', 'lock' ] )
-                       ->getMock();
-               $cache->expects( $this->once() )->method( 'get' )
-                       // .. hits cache
-                       ->willReturn( [
-                               'config' => [ 'known' => 'from-cache' ],
-                               'expires' => INF,
-                               'modifiedIndex' => 0,
-                       ] );
-               $cache->expects( $this->never() )->method( 'lock' );
-
-               // Create config mock
-               $mock = $this->createConfigMock( [
-                       'cache' => $cache,
-               ] );
-               $mock->expects( $this->never() )->method( 'fetchAllFromEtcd' );
-
-               $this->assertSame( 'from-cache', $mock->get( 'known' ) );
-       }
-
-       /**
-        * @covers EtcdConfig::load
-        */
-       public function testLoadProcessCacheHit() {
-               // Create cache mock
-               $cache = $this->getMockBuilder( HashBagOStuff::class )
-                       ->setMethods( [ 'get', 'lock' ] )
-                       ->getMock();
-               $cache->expects( $this->once() )->method( 'get' )
-                       // .. hits cache
-                       ->willReturn( [
-                               'config' => [ 'known' => 'from-cache' ],
-                               'expires' => INF,
-                               'modifiedIndex' => 0,
-                       ] );
-               $cache->expects( $this->never() )->method( 'lock' );
-
-               // Create config mock
-               $mock = $this->createConfigMock( [
-                       'cache' => $cache,
-               ] );
-               $mock->expects( $this->never() )->method( 'fetchAllFromEtcd' );
-
-               $this->assertSame( 'from-cache', $mock->get( 'known' ), 'Cache hit' );
-               $this->assertSame( 'from-cache', $mock->get( 'known' ), 'Process cache hit' );
-       }
-
-       /**
-        * @covers EtcdConfig::load
-        */
-       public function testLoadCacheExpiredLockFetchSucceeded() {
-               // Create cache mock
-               $cache = $this->getMockBuilder( HashBagOStuff::class )
-                       ->setMethods( [ 'get', 'lock' ] )
-                       ->getMock();
-               $cache->expects( $this->once() )->method( 'get' )->willReturn(
-                       // .. stale cache
-                       [
-                               'config' => [ 'known' => 'from-cache-expired' ],
-                               'expires' => -INF,
-                               'modifiedIndex' => 0,
-                       ]
-               );
-               // .. gets lock
-               $cache->expects( $this->once() )->method( 'lock' )
-                       ->willReturn( true );
-
-               // Create config mock
-               $mock = $this->createConfigMock( [
-                       'cache' => $cache,
-               ] );
-               $mock->expects( $this->once() )->method( 'fetchAllFromEtcd' )
-                       ->willReturn( self::createEtcdResponse( [ 'config' => [ 'known' => 'from-fetch' ] ] ) );
-
-               $this->assertSame( 'from-fetch', $mock->get( 'known' ) );
-       }
-
-       /**
-        * @covers EtcdConfig::load
-        */
-       public function testLoadCacheExpiredLockFetchFails() {
-               // Create cache mock
-               $cache = $this->getMockBuilder( HashBagOStuff::class )
-                       ->setMethods( [ 'get', 'lock' ] )
-                       ->getMock();
-               $cache->expects( $this->once() )->method( 'get' )->willReturn(
-                       // .. stale cache
-                       [
-                               'config' => [ 'known' => 'from-cache-expired' ],
-                               'expires' => -INF,
-                               'modifiedIndex' => 0,
-                       ]
-               );
-               // .. gets lock
-               $cache->expects( $this->once() )->method( 'lock' )
-                       ->willReturn( true );
-
-               // Create config mock
-               $mock = $this->createConfigMock( [
-                       'cache' => $cache,
-               ] );
-               $mock->expects( $this->once() )->method( 'fetchAllFromEtcd' )
-                       ->willReturn( self::createEtcdResponse( [ 'error' => 'Fake failure', 'retry' => true ] ) );
-
-               $this->assertSame( 'from-cache-expired', $mock->get( 'known' ) );
-       }
-
-       /**
-        * @covers EtcdConfig::load
-        */
-       public function testLoadCacheExpiredNoLock() {
-               // Create cache mock
-               $cache = $this->getMockBuilder( HashBagOStuff::class )
-                       ->setMethods( [ 'get', 'lock' ] )
-                       ->getMock();
-               $cache->expects( $this->once() )->method( 'get' )
-                       // .. hits cache (expired value)
-                       ->willReturn( [
-                               'config' => [ 'known' => 'from-cache-expired' ],
-                               'expires' => -INF,
-                               'modifiedIndex' => 0,
-                       ] );
-               // .. misses lock
-               $cache->expects( $this->once() )->method( 'lock' )
-                       ->willReturn( false );
-
-               // Create config mock
-               $mock = $this->createConfigMock( [
-                       'cache' => $cache,
-               ] );
-               $mock->expects( $this->never() )->method( 'fetchAllFromEtcd' );
-
-               $this->assertSame( 'from-cache-expired', $mock->get( 'known' ) );
-       }
-
-       public static function provideFetchFromServer() {
-               return [
-                       '200 OK - Success' => [
-                               'http' => [
-                                       'code' => 200,
-                                       'reason' => 'OK',
-                                       'headers' => [],
-                                       'body' => json_encode( [ 'node' => [ 'nodes' => [
-                                               [
-                                                       'key' => '/example/foo',
-                                                       'value' => json_encode( [ 'val' => true ] ),
-                                                       'modifiedIndex' => 123
-                                               ],
-                                       ] ] ] ),
-                                       'error' => '',
-                               ],
-                               'expect' => self::createEtcdResponse( [
-                                       'config' => [ 'foo' => true ], // data
-                                       'modifiedIndex' => 123
-                               ] ),
-                       ],
-                       '200 OK - Empty dir' => [
-                               'http' => [
-                                       'code' => 200,
-                                       'reason' => 'OK',
-                                       'headers' => [],
-                                       'body' => json_encode( [ 'node' => [ 'nodes' => [
-                                               [
-                                                       'key' => '/example/foo',
-                                                       'value' => json_encode( [ 'val' => true ] ),
-                                                       'modifiedIndex' => 123
-                                               ],
-                                               [
-                                                       'key' => '/example/sub',
-                                                       'dir' => true,
-                                                       'modifiedIndex' => 234,
-                                                       'nodes' => [],
-                                               ],
-                                               [
-                                                       'key' => '/example/bar',
-                                                       'value' => json_encode( [ 'val' => false ] ),
-                                                       'modifiedIndex' => 125
-                                               ],
-                                       ] ] ] ),
-                                       'error' => '',
-                               ],
-                               'expect' => self::createEtcdResponse( [
-                                       'config' => [ 'foo' => true, 'bar' => false ], // data
-                                       'modifiedIndex' => 125 // largest modified index
-                               ] ),
-                       ],
-                       '200 OK - Recursive' => [
-                               'http' => [
-                                       'code' => 200,
-                                       'reason' => 'OK',
-                                       'headers' => [],
-                                       'body' => json_encode( [ 'node' => [ 'nodes' => [
-                                               [
-                                                       'key' => '/example/a',
-                                                       'dir' => true,
-                                                       'modifiedIndex' => 124,
-                                                       'nodes' => [
-                                                               [
-                                                                       'key' => 'b',
-                                                                       'value' => json_encode( [ 'val' => true ] ),
-                                                                       'modifiedIndex' => 123,
-
-                                                               ],
-                                                               [
-                                                                       'key' => 'c',
-                                                                       'value' => json_encode( [ 'val' => false ] ),
-                                                                       'modifiedIndex' => 123,
-                                                               ],
-                                                       ],
-                                               ],
-                                       ] ] ] ),
-                                       'error' => '',
-                               ],
-                               'expect' => self::createEtcdResponse( [
-                                       'config' => [ 'a/b' => true, 'a/c' => false ], // data
-                                       'modifiedIndex' => 123 // largest modified index
-                               ] ),
-                       ],
-                       '200 OK - Missing nodes at second level' => [
-                               'http' => [
-                                       'code' => 200,
-                                       'reason' => 'OK',
-                                       'headers' => [],
-                                       'body' => json_encode( [ 'node' => [ 'nodes' => [
-                                               [
-                                                       'key' => '/example/a',
-                                                       'dir' => true,
-                                                       'modifiedIndex' => 0,
-                                               ],
-                                       ] ] ] ),
-                                       'error' => '',
-                               ],
-                               'expect' => self::createEtcdResponse( [
-                                       'error' => "Unexpected JSON response in dir 'a'; missing 'nodes' list.",
-                               ] ),
-                       ],
-                       '200 OK - Directory with non-array "nodes" key' => [
-                               'http' => [
-                                       'code' => 200,
-                                       'reason' => 'OK',
-                                       'headers' => [],
-                                       'body' => json_encode( [ 'node' => [ 'nodes' => [
-                                               [
-                                                       'key' => '/example/a',
-                                                       'dir' => true,
-                                                       'nodes' => 'not an array'
-                                               ],
-                                       ] ] ] ),
-                                       'error' => '',
-                               ],
-                               'expect' => self::createEtcdResponse( [
-                                       'error' => "Unexpected JSON response in dir 'a'; 'nodes' is not an array.",
-                               ] ),
-                       ],
-                       '200 OK - Correctly encoded garbage response' => [
-                               'http' => [
-                                       'code' => 200,
-                                       'reason' => 'OK',
-                                       'headers' => [],
-                                       'body' => json_encode( [ 'foo' => 'bar' ] ),
-                                       'error' => '',
-                               ],
-                               'expect' => self::createEtcdResponse( [
-                                       'error' => "Unexpected JSON response: Missing or invalid node at top level.",
-                               ] ),
-                       ],
-                       '200 OK - Bad value' => [
-                               'http' => [
-                                       'code' => 200,
-                                       'reason' => 'OK',
-                                       'headers' => [],
-                                       'body' => json_encode( [ 'node' => [ 'nodes' => [
-                                               [
-                                                       'key' => '/example/foo',
-                                                       'value' => ';"broken{value',
-                                                       'modifiedIndex' => 123,
-                                               ]
-                                       ] ] ] ),
-                                       'error' => '',
-                               ],
-                               'expect' => self::createEtcdResponse( [
-                                       'error' => "Failed to parse value for 'foo'.",
-                               ] ),
-                       ],
-                       '200 OK - Empty node list' => [
-                               'http' => [
-                                       'code' => 200,
-                                       'reason' => 'OK',
-                                       'headers' => [],
-                                       'body' => '{"node":{"nodes":[], "modifiedIndex": 12 }}',
-                                       'error' => '',
-                               ],
-                               'expect' => self::createEtcdResponse( [
-                                       'config' => [], // data
-                               ] ),
-                       ],
-                       '200 OK - Invalid JSON' => [
-                               'http' => [
-                                       'code' => 200,
-                                       'reason' => 'OK',
-                                       'headers' => [ 'content-length' => 0 ],
-                                       'body' => '',
-                                       'error' => '(curl error: no status set)',
-                               ],
-                               'expect' => self::createEtcdResponse( [
-                                       'error' => "Error unserializing JSON response.",
-                               ] ),
-                       ],
-                       '404 Not Found' => [
-                               'http' => [
-                                       'code' => 404,
-                                       'reason' => 'Not Found',
-                                       'headers' => [ 'content-length' => 0 ],
-                                       'body' => '',
-                                       'error' => '',
-                               ],
-                               'expect' => self::createEtcdResponse( [
-                                       'error' => 'HTTP 404 (Not Found)',
-                               ] ),
-                       ],
-                       '400 Bad Request - custom error' => [
-                               'http' => [
-                                       'code' => 400,
-                                       'reason' => 'Bad Request',
-                                       'headers' => [ 'content-length' => 0 ],
-                                       'body' => '',
-                                       'error' => 'No good reason',
-                               ],
-                               'expect' => self::createEtcdResponse( [
-                                       'error' => 'No good reason',
-                                       'retry' => true, // retry
-                               ] ),
-                       ],
-               ];
-       }
-
-       /**
-        * @covers EtcdConfig::fetchAllFromEtcdServer
-        * @covers EtcdConfig::unserialize
-        * @covers EtcdConfig::parseResponse
-        * @covers EtcdConfig::parseDirectory
-        * @covers EtcdConfigParseError
-        * @dataProvider provideFetchFromServer
-        */
-       public function testFetchFromServer( array $httpResponse, array $expected ) {
-               $http = $this->getMockBuilder( MultiHttpClient::class )
-                       ->disableOriginalConstructor()
-                       ->getMock();
-               $http->expects( $this->once() )->method( 'run' )
-                       ->willReturn( array_values( $httpResponse ) );
-
-               $conf = $this->getMockBuilder( EtcdConfig::class )
-                       ->disableOriginalConstructor()
-                       ->getMock();
-               // Access for protected member and method
-               $conf = TestingAccessWrapper::newFromObject( $conf );
-               $conf->http = $http;
-
-               $this->assertSame(
-                       $expected,
-                       $conf->fetchAllFromEtcdServer( 'etcd-tcp.example.net' )
-               );
-       }
-}
diff --git a/tests/phpunit/includes/config/HashConfigTest.php b/tests/phpunit/includes/config/HashConfigTest.php
deleted file mode 100644 (file)
index bac8311..0000000
+++ /dev/null
@@ -1,63 +0,0 @@
-<?php
-
-class HashConfigTest extends MediaWikiTestCase {
-
-       /**
-        * @covers HashConfig::newInstance
-        */
-       public function testNewInstance() {
-               $conf = HashConfig::newInstance();
-               $this->assertInstanceOf( HashConfig::class, $conf );
-       }
-
-       /**
-        * @covers HashConfig::__construct
-        */
-       public function testConstructor() {
-               $conf = new HashConfig();
-               $this->assertInstanceOf( HashConfig::class, $conf );
-
-               // Test passing arguments to the constructor
-               $conf2 = new HashConfig( [
-                       'one' => '1',
-               ] );
-               $this->assertEquals( '1', $conf2->get( 'one' ) );
-       }
-
-       /**
-        * @covers HashConfig::get
-        */
-       public function testGet() {
-               $conf = new HashConfig( [
-                       'one' => '1',
-               ] );
-               $this->assertEquals( '1', $conf->get( 'one' ) );
-               $this->setExpectedException( ConfigException::class, 'HashConfig::get: undefined option' );
-               $conf->get( 'two' );
-       }
-
-       /**
-        * @covers HashConfig::has
-        */
-       public function testHas() {
-               $conf = new HashConfig( [
-                       'one' => '1',
-               ] );
-               $this->assertTrue( $conf->has( 'one' ) );
-               $this->assertFalse( $conf->has( 'two' ) );
-       }
-
-       /**
-        * @covers HashConfig::set
-        */
-       public function testSet() {
-               $conf = new HashConfig( [
-                       'one' => '1',
-               ] );
-               $conf->set( 'two', '2' );
-               $this->assertEquals( '2', $conf->get( 'two' ) );
-               // Check that set overwrites
-               $conf->set( 'one', '3' );
-               $this->assertEquals( '3', $conf->get( 'one' ) );
-       }
-}
diff --git a/tests/phpunit/includes/config/MultiConfigTest.php b/tests/phpunit/includes/config/MultiConfigTest.php
deleted file mode 100644 (file)
index fc28395..0000000
+++ /dev/null
@@ -1,39 +0,0 @@
-<?php
-
-class MultiConfigTest extends MediaWikiTestCase {
-
-       /**
-        * Tests that settings are fetched in the right order
-        *
-        * @covers MultiConfig::__construct
-        * @covers MultiConfig::get
-        */
-       public function testGet() {
-               $multi = new MultiConfig( [
-                       new HashConfig( [ 'foo' => 'bar' ] ),
-                       new HashConfig( [ 'foo' => 'baz', 'bar' => 'foo' ] ),
-                       new HashConfig( [ 'bar' => 'baz' ] ),
-               ] );
-
-               $this->assertEquals( 'bar', $multi->get( 'foo' ) );
-               $this->assertEquals( 'foo', $multi->get( 'bar' ) );
-               $this->setExpectedException( ConfigException::class, 'MultiConfig::get: undefined option:' );
-               $multi->get( 'notset' );
-       }
-
-       /**
-        * @covers MultiConfig::has
-        */
-       public function testHas() {
-               $conf = new MultiConfig( [
-                       new HashConfig( [ 'foo' => 'foo' ] ),
-                       new HashConfig( [ 'something' => 'bleh' ] ),
-                       new HashConfig( [ 'meh' => 'eh' ] ),
-               ] );
-
-               $this->assertTrue( $conf->has( 'foo' ) );
-               $this->assertTrue( $conf->has( 'something' ) );
-               $this->assertTrue( $conf->has( 'meh' ) );
-               $this->assertFalse( $conf->has( 'what' ) );
-       }
-}
diff --git a/tests/phpunit/includes/config/ServiceOptionsTest.php b/tests/phpunit/includes/config/ServiceOptionsTest.php
deleted file mode 100644 (file)
index 966cf41..0000000
+++ /dev/null
@@ -1,149 +0,0 @@
-<?php
-
-use MediaWiki\Config\ServiceOptions;
-
-/**
- * @coversDefaultClass \MediaWiki\Config\ServiceOptions
- */
-class ServiceOptionsTest extends MediaWikiTestCase {
-       public static $testObj;
-
-       public static function setUpBeforeClass() {
-               parent::setUpBeforeClass();
-
-               self::$testObj = new stdclass();
-       }
-
-       /**
-        * @dataProvider provideConstructor
-        * @covers ::__construct
-        * @covers ::assertRequiredOptions
-        * @covers ::get
-        */
-       public function testConstructor( $expected, $keys, ...$sources ) {
-               $options = new ServiceOptions( $keys, ...$sources );
-
-               foreach ( $expected as $key => $val ) {
-                       $this->assertSame( $val, $options->get( $key ) );
-               }
-
-               // This is lumped in the same test because there's no support for depending on a test that
-               // has a data provider.
-               $options->assertRequiredOptions( array_keys( $expected ) );
-
-               // Suppress warning if no assertions were run. This is expected for empty arguments.
-               $this->assertTrue( true );
-       }
-
-       public function provideConstructor() {
-               return [
-                       'No keys' => [ [], [], [ 'a' => 'aval' ] ],
-                       'Simple array source' => [
-                               [ 'a' => 'aval', 'b' => 'bval' ],
-                               [ 'a', 'b' ],
-                               [ 'a' => 'aval', 'b' => 'bval', 'c' => 'cval' ],
-                       ],
-                       'Simple HashConfig source' => [
-                               [ 'a' => 'aval', 'b' => 'bval' ],
-                               [ 'a', 'b' ],
-                               new HashConfig( [ 'a' => 'aval', 'b' => 'bval', 'c' => 'cval' ] ),
-                       ],
-                       'Three different sources' => [
-                               [ 'a' => 'aval', 'b' => 'bval' ],
-                               [ 'a', 'b' ],
-                               [ 'z' => 'zval' ],
-                               new HashConfig( [ 'a' => 'aval', 'c' => 'cval' ] ),
-                               [ 'b' => 'bval', 'd' => 'dval' ],
-                       ],
-                       'null key' => [
-                               [ 'a' => null ],
-                               [ 'a' ],
-                               [ 'a' => null ],
-                       ],
-                       'Numeric option name' => [
-                               [ '0' => 'nothing' ],
-                               [ '0' ],
-                               [ '0' => 'nothing' ],
-                       ],
-                       'Multiple sources for one key' => [
-                               [ 'a' => 'winner' ],
-                               [ 'a' ],
-                               [ 'a' => 'winner' ],
-                               [ 'a' => 'second place' ],
-                       ],
-                       'Object value is passed by reference' => [
-                               [ 'a' => self::$testObj ],
-                               [ 'a' ],
-                               [ 'a' => self::$testObj ],
-                       ],
-               ];
-       }
-
-       /**
-        * @covers ::__construct
-        */
-       public function testKeyNotFound() {
-               $this->setExpectedException( InvalidArgumentException::class,
-                       'Key "a" not found in input sources' );
-
-               new ServiceOptions( [ 'a' ], [ 'b' => 'bval' ], [ 'c' => 'cval' ] );
-       }
-
-       /**
-        * @covers ::__construct
-        * @covers ::assertRequiredOptions
-        */
-       public function testOutOfOrderAssertRequiredOptions() {
-               $options = new ServiceOptions( [ 'a', 'b' ], [ 'a' => '', 'b' => '' ] );
-               $options->assertRequiredOptions( [ 'b', 'a' ] );
-               $this->assertTrue( true, 'No exception thrown' );
-       }
-
-       /**
-        * @covers ::__construct
-        * @covers ::get
-        */
-       public function testGetUnrecognized() {
-               $this->setExpectedException( InvalidArgumentException::class,
-                       'Unrecognized option "b"' );
-
-               $options = new ServiceOptions( [ 'a' ], [ 'a' => '' ] );
-               $options->get( 'b' );
-       }
-
-       /**
-        * @covers ::__construct
-        * @covers ::assertRequiredOptions
-        */
-       public function testExtraKeys() {
-               $this->setExpectedException( Wikimedia\Assert\PreconditionException::class,
-                       'Precondition failed: Unsupported options passed: b, c!' );
-
-               $options = new ServiceOptions( [ 'a', 'b', 'c' ], [ 'a' => '', 'b' => '', 'c' => '' ] );
-               $options->assertRequiredOptions( [ 'a' ] );
-       }
-
-       /**
-        * @covers ::__construct
-        * @covers ::assertRequiredOptions
-        */
-       public function testMissingKeys() {
-               $this->setExpectedException( Wikimedia\Assert\PreconditionException::class,
-                       'Precondition failed: Required options missing: a, b!' );
-
-               $options = new ServiceOptions( [ 'c' ], [ 'c' => '' ] );
-               $options->assertRequiredOptions( [ 'a', 'b', 'c' ] );
-       }
-
-       /**
-        * @covers ::__construct
-        * @covers ::assertRequiredOptions
-        */
-       public function testExtraAndMissingKeys() {
-               $this->setExpectedException( Wikimedia\Assert\PreconditionException::class,
-                       'Precondition failed: Unsupported options passed: b! Required options missing: c!' );
-
-               $options = new ServiceOptions( [ 'a', 'b' ], [ 'a' => '', 'b' => '' ] );
-               $options->assertRequiredOptions( [ 'a', 'c' ] );
-       }
-}
diff --git a/tests/phpunit/includes/content/JsonContentHandlerTest.php b/tests/phpunit/includes/content/JsonContentHandlerTest.php
deleted file mode 100644 (file)
index abfb673..0000000
+++ /dev/null
@@ -1,14 +0,0 @@
-<?php
-
-class JsonContentHandlerTest extends MediaWikiTestCase {
-
-       /**
-        * @covers JsonContentHandler::makeEmptyContent
-        */
-       public function testMakeEmptyContent() {
-               $handler = new JsonContentHandler();
-               $content = $handler->makeEmptyContent();
-               $this->assertInstanceOf( JsonContent::class, $content );
-               $this->assertTrue( $content->isValid() );
-       }
-}
diff --git a/tests/phpunit/includes/db/DatabaseOracleTest.php b/tests/phpunit/includes/db/DatabaseOracleTest.php
deleted file mode 100644 (file)
index 061e121..0000000
+++ /dev/null
@@ -1,52 +0,0 @@
-<?php
-
-class DatabaseOracleTest extends PHPUnit\Framework\TestCase {
-
-       use MediaWikiCoversValidator;
-       use PHPUnit4And6Compat;
-
-       /**
-        * @return PHPUnit_Framework_MockObject_MockObject|DatabaseOracle
-        */
-       private function getMockDb() {
-               return $this->getMockBuilder( DatabaseOracle::class )
-                       ->disableOriginalConstructor()
-                       ->setMethods( null )
-                       ->getMock();
-       }
-
-       public function provideBuildSubstring() {
-               yield [ 'someField', 1, 2, 'SUBSTR(someField,1,2)' ];
-               yield [ 'someField', 1, null, 'SUBSTR(someField,1)' ];
-       }
-
-       /**
-        * @covers DatabaseOracle::buildSubstring
-        * @dataProvider provideBuildSubstring
-        */
-       public function testBuildSubstring( $input, $start, $length, $expected ) {
-               $mockDb = $this->getMockDb();
-               $output = $mockDb->buildSubstring( $input, $start, $length );
-               $this->assertSame( $expected, $output );
-       }
-
-       public function provideBuildSubstring_invalidParams() {
-               yield [ -1, 1 ];
-               yield [ 1, -1 ];
-               yield [ 1, 'foo' ];
-               yield [ 'foo', 1 ];
-               yield [ null, 1 ];
-               yield [ 0, 1 ];
-       }
-
-       /**
-        * @covers DatabaseOracle::buildSubstring
-        * @dataProvider provideBuildSubstring_invalidParams
-        */
-       public function testBuildSubstring_invalidParams( $start, $length ) {
-               $mockDb = $this->getMockDb();
-               $this->setExpectedException( InvalidArgumentException::class );
-               $mockDb->buildSubstring( 'foo', $start, $length );
-       }
-
-}
diff --git a/tests/phpunit/includes/debug/MWDebugTest.php b/tests/phpunit/includes/debug/MWDebugTest.php
deleted file mode 100644 (file)
index 6f0b1db..0000000
+++ /dev/null
@@ -1,140 +0,0 @@
-<?php
-
-class MWDebugTest extends MediaWikiTestCase {
-
-       protected function setUp() {
-               parent::setUp();
-               /** Clear log before each test */
-               MWDebug::clearLog();
-       }
-
-       public static function setUpBeforeClass() {
-               parent::setUpBeforeClass();
-               MWDebug::init();
-               Wikimedia\suppressWarnings();
-       }
-
-       public static function tearDownAfterClass() {
-               parent::tearDownAfterClass();
-               MWDebug::deinit();
-               Wikimedia\restoreWarnings();
-       }
-
-       /**
-        * @covers MWDebug::log
-        */
-       public function testAddLog() {
-               MWDebug::log( 'logging a string' );
-               $this->assertEquals(
-                       [ [
-                               'msg' => 'logging a string',
-                               'type' => 'log',
-                               'caller' => 'MWDebugTest->testAddLog',
-                       ] ],
-                       MWDebug::getLog()
-               );
-       }
-
-       /**
-        * @covers MWDebug::warning
-        */
-       public function testAddWarning() {
-               MWDebug::warning( 'Warning message' );
-               $this->assertEquals(
-                       [ [
-                               'msg' => 'Warning message',
-                               'type' => 'warn',
-                               'caller' => 'MWDebugTest::testAddWarning',
-                       ] ],
-                       MWDebug::getLog()
-               );
-       }
-
-       /**
-        * @covers MWDebug::deprecated
-        */
-       public function testAvoidDuplicateDeprecations() {
-               MWDebug::deprecated( 'wfOldFunction', '1.0', 'component' );
-               MWDebug::deprecated( 'wfOldFunction', '1.0', 'component' );
-
-               // assertCount() not available on WMF integration server
-               $this->assertEquals( 1,
-                       count( MWDebug::getLog() ),
-                       "Only one deprecated warning per function should be kept"
-               );
-       }
-
-       /**
-        * @covers MWDebug::deprecated
-        */
-       public function testAvoidNonConsecutivesDuplicateDeprecations() {
-               MWDebug::deprecated( 'wfOldFunction', '1.0', 'component' );
-               MWDebug::warning( 'some warning' );
-               MWDebug::log( 'we could have logged something too' );
-               // Another deprecation
-               MWDebug::deprecated( 'wfOldFunction', '1.0', 'component' );
-
-               // assertCount() not available on WMF integration server
-               $this->assertEquals( 3,
-                       count( MWDebug::getLog() ),
-                       "Only one deprecated warning per function should be kept"
-               );
-       }
-
-       /**
-        * @covers MWDebug::appendDebugInfoToApiResult
-        */
-       public function testAppendDebugInfoToApiResultXmlFormat() {
-               $request = $this->newApiRequest(
-                       [ 'action' => 'help', 'format' => 'xml' ],
-                       '/api.php?action=help&format=xml'
-               );
-
-               $context = new RequestContext();
-               $context->setRequest( $request );
-
-               $apiMain = new ApiMain( $context );
-
-               $result = new ApiResult( $apiMain );
-
-               MWDebug::appendDebugInfoToApiResult( $context, $result );
-
-               $this->assertInstanceOf( ApiResult::class, $result );
-               $data = $result->getResultData();
-
-               $expectedKeys = [ 'mwVersion', 'phpEngine', 'phpVersion', 'gitRevision', 'gitBranch',
-                       'gitViewUrl', 'time', 'log', 'debugLog', 'queries', 'request', 'memory',
-                       'memoryPeak', 'includes', '_element' ];
-
-               foreach ( $expectedKeys as $expectedKey ) {
-                       $this->assertArrayHasKey( $expectedKey, $data['debuginfo'], "debuginfo has $expectedKey" );
-               }
-
-               $xml = ApiFormatXml::recXmlPrint( 'help', $data, null );
-
-               // exception not thrown
-               $this->assertInternalType( 'string', $xml );
-       }
-
-       /**
-        * @param string[] $params
-        * @param string $requestUrl
-        *
-        * @return FauxRequest
-        */
-       private function newApiRequest( array $params, $requestUrl ) {
-               $request = $this->getMockBuilder( FauxRequest::class )
-                       ->setMethods( [ 'getRequestURL' ] )
-                       ->setConstructorArgs( [
-                               $params
-                       ] )
-                       ->getMock();
-
-               $request->expects( $this->any() )
-                       ->method( 'getRequestURL' )
-                       ->will( $this->returnValue( $requestUrl ) );
-
-               return $request;
-       }
-
-}
diff --git a/tests/phpunit/includes/debug/logger/MonologSpiTest.php b/tests/phpunit/includes/debug/logger/MonologSpiTest.php
deleted file mode 100644 (file)
index fda3ac6..0000000
+++ /dev/null
@@ -1,136 +0,0 @@
-<?php
-/**
- * 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; either version 2 of the License, or
- * (at your option) any later version.
- *
- * 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.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- */
-
-namespace MediaWiki\Logger;
-
-use MediaWikiTestCase;
-use Wikimedia\TestingAccessWrapper;
-
-class MonologSpiTest extends MediaWikiTestCase {
-
-       /**
-        * @covers MediaWiki\Logger\MonologSpi::mergeConfig
-        */
-       public function testMergeConfig() {
-               $base = [
-                       'loggers' => [
-                               '@default' => [
-                                       'processors' => [ 'constructor' ],
-                                       'handlers' => [ 'constructor' ],
-                               ],
-                       ],
-                       'processors' => [
-                               'constructor' => [
-                                       'class' => 'constructor',
-                               ],
-                       ],
-                       'handlers' => [
-                               'constructor' => [
-                                       'class' => 'constructor',
-                                       'formatter' => 'constructor',
-                               ],
-                       ],
-                       'formatters' => [
-                               'constructor' => [
-                                       'class' => 'constructor',
-                               ],
-                       ],
-               ];
-
-               $fixture = new MonologSpi( $base );
-               $this->assertSame(
-                       $base,
-                       TestingAccessWrapper::newFromObject( $fixture )->config
-               );
-
-               $fixture->mergeConfig( [
-                       'loggers' => [
-                               'merged' => [
-                                       'processors' => [ 'merged' ],
-                                       'handlers' => [ 'merged' ],
-                               ],
-                       ],
-                       'processors' => [
-                               'merged' => [
-                                       'class' => 'merged',
-                               ],
-                       ],
-                       'magic' => [
-                               'idkfa' => [ 'xyzzy' ],
-                       ],
-                       'handlers' => [
-                               'merged' => [
-                                       'class' => 'merged',
-                                       'formatter' => 'merged',
-                               ],
-                       ],
-                       'formatters' => [
-                               'merged' => [
-                                       'class' => 'merged',
-                               ],
-                       ],
-               ] );
-               $this->assertSame(
-                       [
-                               'loggers' => [
-                                       '@default' => [
-                                               'processors' => [ 'constructor' ],
-                                               'handlers' => [ 'constructor' ],
-                                       ],
-                                       'merged' => [
-                                               'processors' => [ 'merged' ],
-                                               'handlers' => [ 'merged' ],
-                                       ],
-                               ],
-                               'processors' => [
-                                       'constructor' => [
-                                               'class' => 'constructor',
-                                       ],
-                                       'merged' => [
-                                               'class' => 'merged',
-                                       ],
-                               ],
-                               'handlers' => [
-                                       'constructor' => [
-                                               'class' => 'constructor',
-                                               'formatter' => 'constructor',
-                                       ],
-                                       'merged' => [
-                                               'class' => 'merged',
-                                               'formatter' => 'merged',
-                                       ],
-                               ],
-                               'formatters' => [
-                                       'constructor' => [
-                                               'class' => 'constructor',
-                                       ],
-                                       'merged' => [
-                                               'class' => 'merged',
-                                       ],
-                               ],
-                               'magic' => [
-                                       'idkfa' => [ 'xyzzy' ],
-                               ],
-                       ],
-                       TestingAccessWrapper::newFromObject( $fixture )->config
-               );
-       }
-
-}
diff --git a/tests/phpunit/includes/debug/logger/monolog/AvroFormatterTest.php b/tests/phpunit/includes/debug/logger/monolog/AvroFormatterTest.php
deleted file mode 100644 (file)
index baa4df7..0000000
+++ /dev/null
@@ -1,76 +0,0 @@
-<?php
-/**
- * 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; either version 2 of the License, or
- * (at your option) any later version.
- *
- * 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.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- */
-
-namespace MediaWiki\Logger\Monolog;
-
-use MediaWikiTestCase;
-use PHPUnit_Framework_Error_Notice;
-
-/**
- * @covers \MediaWiki\Logger\Monolog\AvroFormatter
- */
-class AvroFormatterTest extends MediaWikiTestCase {
-
-       protected function setUp() {
-               if ( !class_exists( 'AvroStringIO' ) ) {
-                       $this->markTestSkipped( 'Avro is required for the AvroFormatterTest' );
-               }
-               parent::setUp();
-       }
-
-       public function testSchemaNotAvailable() {
-               $formatter = new AvroFormatter( [] );
-               $this->setExpectedException(
-                       'PHPUnit_Framework_Error_Notice',
-                       "The schema for channel 'marty' is not available"
-               );
-               $formatter->format( [ 'channel' => 'marty' ] );
-       }
-
-       public function testSchemaNotAvailableReturnValue() {
-               $formatter = new AvroFormatter( [] );
-               $noticeEnabled = PHPUnit_Framework_Error_Notice::$enabled;
-               // disable conversion of notices
-               PHPUnit_Framework_Error_Notice::$enabled = false;
-               // have to keep the user notice from being output
-               \Wikimedia\suppressWarnings();
-               $res = $formatter->format( [ 'channel' => 'marty' ] );
-               \Wikimedia\restoreWarnings();
-               PHPUnit_Framework_Error_Notice::$enabled = $noticeEnabled;
-               $this->assertNull( $res );
-       }
-
-       public function testDoesSomethingWhenSchemaAvailable() {
-               $formatter = new AvroFormatter( [
-                       'string' => [
-                               'schema' => [ 'type' => 'string' ],
-                               'revision' => 1010101,
-                       ]
-               ] );
-               $res = $formatter->format( [
-                       'channel' => 'string',
-                       'context' => 'better to be',
-               ] );
-               $this->assertNotNull( $res );
-               // basically just tell us if avro changes its string encoding, or if
-               // we completely fail to generate a log message.
-               $this->assertEquals( 'AAAAAAAAD2m1GGJldHRlciB0byBiZQ==', base64_encode( $res ) );
-       }
-}
diff --git a/tests/phpunit/includes/debug/logger/monolog/CeeFormatterTest.php b/tests/phpunit/includes/debug/logger/monolog/CeeFormatterTest.php
deleted file mode 100644 (file)
index b30c7a4..0000000
+++ /dev/null
@@ -1,20 +0,0 @@
-<?php
-
-namespace MediaWiki\Logger\Monolog;
-
-/**
- * Flay per https://phabricator.wikimedia.org/T218688.
- *
- * @group Broken
- * @covers \MediaWiki\Logger\Monolog\CeeFormatter
- */
-class CeeFormatterTest extends \PHPUnit\Framework\TestCase {
-       public function testV1() {
-               $ls_formatter = new LogstashFormatter( 'app', 'system', null, 'ctx_', LogstashFormatter::V1 );
-               $cee_formatter = new CeeFormatter( 'app', 'system', null, 'ctx_', LogstashFormatter::V1 );
-               $record = [ 'extra' => [ 'url' => 1 ], 'context' => [ 'url' => 2 ] ];
-               $this->assertSame(
-                       $cee_formatter->format( $record ),
-                       "@cee: " . $ls_formatter->format( $record ) );
-       }
-}
diff --git a/tests/phpunit/includes/debug/logger/monolog/KafkaHandlerTest.php b/tests/phpunit/includes/debug/logger/monolog/KafkaHandlerTest.php
deleted file mode 100644 (file)
index 4c0ca04..0000000
+++ /dev/null
@@ -1,227 +0,0 @@
-<?php
-/**
- * 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; either version 2 of the License, or
- * (at your option) any later version.
- *
- * 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.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- */
-
-namespace MediaWiki\Logger\Monolog;
-
-use MediaWikiTestCase;
-use Monolog\Logger;
-use Wikimedia\TestingAccessWrapper;
-
-/**
- * @covers \MediaWiki\Logger\Monolog\KafkaHandler
- */
-class KafkaHandlerTest extends MediaWikiTestCase {
-
-       protected function setUp() {
-               if ( !class_exists( 'Monolog\Handler\AbstractProcessingHandler' )
-                       || !class_exists( 'Kafka\Produce' )
-               ) {
-                       $this->markTestSkipped( 'Monolog and Kafka are required for the KafkaHandlerTest' );
-               }
-
-               parent::setUp();
-       }
-
-       public function topicNamingProvider() {
-               return [
-                       [ [], 'monolog_foo' ],
-                       [ [ 'alias' => [ 'foo' => 'bar' ] ], 'bar' ]
-               ];
-       }
-
-       /**
-        * @dataProvider topicNamingProvider
-        */
-       public function testTopicNaming( $options, $expect ) {
-               $produce = $this->getMockBuilder( 'Kafka\Produce' )
-                       ->disableOriginalConstructor()
-                       ->getMock();
-               $produce->expects( $this->any() )
-                       ->method( 'getAvailablePartitions' )
-                       ->will( $this->returnValue( [ 'A' ] ) );
-               $produce->expects( $this->once() )
-                       ->method( 'setMessages' )
-                       ->with( $expect, $this->anything(), $this->anything() );
-               $produce->expects( $this->any() )
-                       ->method( 'send' )
-                       ->will( $this->returnValue( true ) );
-
-               $handler = new KafkaHandler( $produce, $options );
-               $handler->handle( [
-                       'channel' => 'foo',
-                       'level' => Logger::EMERGENCY,
-                       'extra' => [],
-                       'context' => [],
-               ] );
-       }
-
-       public function swallowsExceptionsWhenRequested() {
-               return [
-                       // defaults to false
-                       [ [], true ],
-                       // also try false explicitly
-                       [ [ 'swallowExceptions' => false ], true ],
-                       // turn it on
-                       [ [ 'swallowExceptions' => true ], false ],
-               ];
-       }
-
-       /**
-        * @dataProvider swallowsExceptionsWhenRequested
-        */
-       public function testGetAvailablePartitionsException( $options, $expectException ) {
-               $produce = $this->getMockBuilder( 'Kafka\Produce' )
-                       ->disableOriginalConstructor()
-                       ->getMock();
-               $produce->expects( $this->any() )
-                       ->method( 'getAvailablePartitions' )
-                       ->will( $this->throwException( new \Kafka\Exception ) );
-               $produce->expects( $this->any() )
-                       ->method( 'send' )
-                       ->will( $this->returnValue( true ) );
-
-               if ( $expectException ) {
-                       $this->setExpectedException( 'Kafka\Exception' );
-               }
-
-               $handler = new KafkaHandler( $produce, $options );
-               $handler->handle( [
-                       'channel' => 'foo',
-                       'level' => Logger::EMERGENCY,
-                       'extra' => [],
-                       'context' => [],
-               ] );
-
-               if ( !$expectException ) {
-                       $this->assertTrue( true, 'no exception was thrown' );
-               }
-       }
-
-       /**
-        * @dataProvider swallowsExceptionsWhenRequested
-        */
-       public function testSendException( $options, $expectException ) {
-               $produce = $this->getMockBuilder( 'Kafka\Produce' )
-                       ->disableOriginalConstructor()
-                       ->getMock();
-               $produce->expects( $this->any() )
-                       ->method( 'getAvailablePartitions' )
-                       ->will( $this->returnValue( [ 'A' ] ) );
-               $produce->expects( $this->any() )
-                       ->method( 'send' )
-                       ->will( $this->throwException( new \Kafka\Exception ) );
-
-               if ( $expectException ) {
-                       $this->setExpectedException( 'Kafka\Exception' );
-               }
-
-               $handler = new KafkaHandler( $produce, $options );
-               $handler->handle( [
-                       'channel' => 'foo',
-                       'level' => Logger::EMERGENCY,
-                       'extra' => [],
-                       'context' => [],
-               ] );
-
-               if ( !$expectException ) {
-                       $this->assertTrue( true, 'no exception was thrown' );
-               }
-       }
-
-       public function testHandlesNullFormatterResult() {
-               $produce = $this->getMockBuilder( 'Kafka\Produce' )
-                       ->disableOriginalConstructor()
-                       ->getMock();
-               $produce->expects( $this->any() )
-                       ->method( 'getAvailablePartitions' )
-                       ->will( $this->returnValue( [ 'A' ] ) );
-               $mockMethod = $produce->expects( $this->exactly( 2 ) )
-                       ->method( 'setMessages' );
-               $produce->expects( $this->any() )
-                       ->method( 'send' )
-                       ->will( $this->returnValue( true ) );
-               // evil hax
-               $matcher = TestingAccessWrapper::newFromObject( $mockMethod )->matcher;
-               TestingAccessWrapper::newFromObject( $matcher )->parametersMatcher =
-                       new \PHPUnit_Framework_MockObject_Matcher_ConsecutiveParameters( [
-                               [ $this->anything(), $this->anything(), [ 'words' ] ],
-                               [ $this->anything(), $this->anything(), [ 'lines' ] ]
-                       ] );
-
-               $formatter = $this->createMock( \Monolog\Formatter\FormatterInterface::class );
-               $formatter->expects( $this->any() )
-                       ->method( 'format' )
-                       ->will( $this->onConsecutiveCalls( 'words', null, 'lines' ) );
-
-               $handler = new KafkaHandler( $produce, [] );
-               $handler->setFormatter( $formatter );
-               for ( $i = 0; $i < 3; ++$i ) {
-                       $handler->handle( [
-                               'channel' => 'foo',
-                               'level' => Logger::EMERGENCY,
-                               'extra' => [],
-                               'context' => [],
-                       ] );
-               }
-       }
-
-       public function testBatchHandlesNullFormatterResult() {
-               $produce = $this->getMockBuilder( 'Kafka\Produce' )
-                       ->disableOriginalConstructor()
-                       ->getMock();
-               $produce->expects( $this->any() )
-                       ->method( 'getAvailablePartitions' )
-                       ->will( $this->returnValue( [ 'A' ] ) );
-               $produce->expects( $this->once() )
-                       ->method( 'setMessages' )
-                       ->with( $this->anything(), $this->anything(), [ 'words', 'lines' ] );
-               $produce->expects( $this->any() )
-                       ->method( 'send' )
-                       ->will( $this->returnValue( true ) );
-
-               $formatter = $this->createMock( \Monolog\Formatter\FormatterInterface::class );
-               $formatter->expects( $this->any() )
-                       ->method( 'format' )
-                       ->will( $this->onConsecutiveCalls( 'words', null, 'lines' ) );
-
-               $handler = new KafkaHandler( $produce, [] );
-               $handler->setFormatter( $formatter );
-               $handler->handleBatch( [
-                       [
-                               'channel' => 'foo',
-                               'level' => Logger::EMERGENCY,
-                               'extra' => [],
-                               'context' => [],
-                       ],
-                       [
-                               'channel' => 'foo',
-                               'level' => Logger::EMERGENCY,
-                               'extra' => [],
-                               'context' => [],
-                       ],
-                       [
-                               'channel' => 'foo',
-                               'level' => Logger::EMERGENCY,
-                               'extra' => [],
-                               'context' => [],
-                       ],
-               ] );
-       }
-}
diff --git a/tests/phpunit/includes/debug/logger/monolog/LineFormatterTest.php b/tests/phpunit/includes/debug/logger/monolog/LineFormatterTest.php
deleted file mode 100644 (file)
index bdd5c81..0000000
+++ /dev/null
@@ -1,122 +0,0 @@
-<?php
-/**
- * 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; either version 2 of the License, or
- * (at your option) any later version.
- *
- * 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.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- */
-
-namespace MediaWiki\Logger\Monolog;
-
-use AssertionError;
-use InvalidArgumentException;
-use LengthException;
-use LogicException;
-use MediaWikiTestCase;
-use Wikimedia\TestingAccessWrapper;
-
-class LineFormatterTest extends MediaWikiTestCase {
-
-       protected function setUp() {
-               if ( !class_exists( 'Monolog\Formatter\LineFormatter' ) ) {
-                       $this->markTestSkipped( 'This test requires monolog to be installed' );
-               }
-               parent::setUp();
-       }
-
-       /**
-        * @covers MediaWiki\Logger\Monolog\LineFormatter::normalizeException
-        */
-       public function testNormalizeExceptionNoTrace() {
-               $fixture = new LineFormatter();
-               $fixture->includeStacktraces( false );
-               $fixture = TestingAccessWrapper::newFromObject( $fixture );
-               $boom = new InvalidArgumentException( 'boom', 0,
-                       new LengthException( 'too long', 0,
-                               new LogicException( 'Spock wuz here' )
-                       )
-               );
-               $out = $fixture->normalizeException( $boom );
-               $this->assertContains( "\n[Exception InvalidArgumentException]", $out );
-               $this->assertContains( "\nCaused by: [Exception LengthException]", $out );
-               $this->assertContains( "\nCaused by: [Exception LogicException]", $out );
-               $this->assertNotContains( "\n  #0", $out );
-       }
-
-       /**
-        * @covers MediaWiki\Logger\Monolog\LineFormatter::normalizeException
-        */
-       public function testNormalizeExceptionTrace() {
-               $fixture = new LineFormatter();
-               $fixture->includeStacktraces( true );
-               $fixture = TestingAccessWrapper::newFromObject( $fixture );
-               $boom = new InvalidArgumentException( 'boom', 0,
-                       new LengthException( 'too long', 0,
-                               new LogicException( 'Spock wuz here' )
-                       )
-               );
-               $out = $fixture->normalizeException( $boom );
-               $this->assertContains( "\n[Exception InvalidArgumentException]", $out );
-               $this->assertContains( "\nCaused by: [Exception LengthException]", $out );
-               $this->assertContains( "\nCaused by: [Exception LogicException]", $out );
-               $this->assertContains( "\n  #0", $out );
-       }
-
-       /**
-        * @covers MediaWiki\Logger\Monolog\LineFormatter::normalizeException
-        */
-       public function testNormalizeExceptionErrorNoTrace() {
-               if ( !class_exists( AssertionError::class ) ) {
-                       $this->markTestSkipped( 'AssertionError class does not exist' );
-               }
-
-               $fixture = new LineFormatter();
-               $fixture->includeStacktraces( false );
-               $fixture = TestingAccessWrapper::newFromObject( $fixture );
-               $boom = new InvalidArgumentException( 'boom', 0,
-                       new LengthException( 'too long', 0,
-                               new AssertionError( 'Spock wuz here' )
-                       )
-               );
-               $out = $fixture->normalizeException( $boom );
-               $this->assertContains( "\n[Exception InvalidArgumentException]", $out );
-               $this->assertContains( "\nCaused by: [Exception LengthException]", $out );
-               $this->assertContains( "\nCaused by: [Error AssertionError]", $out );
-               $this->assertNotContains( "\n  #0", $out );
-       }
-
-       /**
-        * @covers MediaWiki\Logger\Monolog\LineFormatter::normalizeException
-        */
-       public function testNormalizeExceptionErrorTrace() {
-               if ( !class_exists( AssertionError::class ) ) {
-                       $this->markTestSkipped( 'AssertionError class does not exist' );
-               }
-
-               $fixture = new LineFormatter();
-               $fixture->includeStacktraces( true );
-               $fixture = TestingAccessWrapper::newFromObject( $fixture );
-               $boom = new InvalidArgumentException( 'boom', 0,
-                       new LengthException( 'too long', 0,
-                               new AssertionError( 'Spock wuz here' )
-                       )
-               );
-               $out = $fixture->normalizeException( $boom );
-               $this->assertContains( "\n[Exception InvalidArgumentException]", $out );
-               $this->assertContains( "\nCaused by: [Exception LengthException]", $out );
-               $this->assertContains( "\nCaused by: [Error AssertionError]", $out );
-               $this->assertContains( "\n  #0", $out );
-       }
-}
diff --git a/tests/phpunit/includes/debug/logger/monolog/LogstashFormatterTest.php b/tests/phpunit/includes/debug/logger/monolog/LogstashFormatterTest.php
deleted file mode 100644 (file)
index a1207b2..0000000
+++ /dev/null
@@ -1,59 +0,0 @@
-<?php
-
-namespace MediaWiki\Logger\Monolog;
-
-class LogstashFormatterTest extends \PHPUnit\Framework\TestCase {
-       /**
-        * @dataProvider provideV1
-        * @covers MediaWiki\Logger\Monolog\LogstashFormatter::formatV1
-        * @param array $record The input record.
-        * @param array $expected Associative array of expected keys and their values.
-        * @param array $notExpected List of keys that should not exist.
-        */
-       public function testV1( array $record, array $expected, array $notExpected ) {
-               $formatter = new LogstashFormatter( 'app', 'system', null, null, LogstashFormatter::V1 );
-               $formatted = json_decode( $formatter->format( $record ), true );
-               foreach ( $expected as $key => $value ) {
-                       $this->assertArrayHasKey( $key, $formatted );
-                       $this->assertSame( $value, $formatted[$key] );
-               }
-               foreach ( $notExpected as $key ) {
-                       $this->assertArrayNotHasKey( $key, $formatted );
-               }
-       }
-
-       public function provideV1() {
-               return [
-                       [
-                               [ 'extra' => [ 'foo' => 1 ], 'context' => [ 'bar' => 2 ] ],
-                               [ 'foo' => 1, 'bar' => 2 ],
-                               [ 'logstash_formatter_key_conflict' ],
-                       ],
-                       [
-                               [ 'extra' => [ 'url' => 1 ], 'context' => [ 'url' => 2 ] ],
-                               [ 'url' => 1, 'c_url' => 2, 'logstash_formatter_key_conflict' => [ 'url' ] ],
-                               [],
-                       ],
-                       [
-                               [ 'channel' => 'x', 'context' => [ 'channel' => 'y' ] ],
-                               [ 'channel' => 'x', 'c_channel' => 'y',
-                                       'logstash_formatter_key_conflict' => [ 'channel' ] ],
-                               [],
-                       ],
-               ];
-       }
-
-       /**
-        * @covers MediaWiki\Logger\Monolog\LogstashFormatter::formatV1
-        */
-       public function testV1WithPrefix() {
-               $formatter = new LogstashFormatter( 'app', 'system', null, 'ctx_', LogstashFormatter::V1 );
-               $record = [ 'extra' => [ 'url' => 1 ], 'context' => [ 'url' => 2 ] ];
-               $formatted = json_decode( $formatter->format( $record ), true );
-               $this->assertArrayHasKey( 'url', $formatted );
-               $this->assertSame( 1, $formatted['url'] );
-               $this->assertArrayHasKey( 'ctx_url', $formatted );
-               $this->assertSame( 2, $formatted['ctx_url'] );
-               $this->assertArrayNotHasKey( 'c_url', $formatted );
-       }
-}
diff --git a/tests/phpunit/includes/deferred/MWCallableUpdateTest.php b/tests/phpunit/includes/deferred/MWCallableUpdateTest.php
deleted file mode 100644 (file)
index 3ab9b56..0000000
+++ /dev/null
@@ -1,82 +0,0 @@
-<?php
-
-/**
- * @covers MWCallableUpdate
- */
-class MWCallableUpdateTest extends PHPUnit\Framework\TestCase {
-
-       use MediaWikiCoversValidator;
-
-       public function testDoUpdate() {
-               $ran = 0;
-               $update = new MWCallableUpdate( function () use ( &$ran ) {
-                       $ran++;
-               } );
-               $this->assertSame( 0, $ran );
-               $update->doUpdate();
-               $this->assertSame( 1, $ran );
-       }
-
-       public function testCancel() {
-               // Prepare update and DB
-               $db = new DatabaseTestHelper( __METHOD__ );
-               $db->begin( __METHOD__ );
-               $ran = 0;
-               $update = new MWCallableUpdate( function () use ( &$ran ) {
-                       $ran++;
-               }, __METHOD__, $db );
-
-               // Emulate rollback
-               $db->rollback( __METHOD__ );
-
-               $update->doUpdate();
-
-               // Ensure it was cancelled
-               $this->assertSame( 0, $ran );
-       }
-
-       public function testCancelSome() {
-               // Prepare update and DB
-               $db1 = new DatabaseTestHelper( __METHOD__ );
-               $db1->begin( __METHOD__ );
-               $db2 = new DatabaseTestHelper( __METHOD__ );
-               $db2->begin( __METHOD__ );
-               $ran = 0;
-               $update = new MWCallableUpdate( function () use ( &$ran ) {
-                       $ran++;
-               }, __METHOD__, [ $db1, $db2 ] );
-
-               // Emulate rollback
-               $db1->rollback( __METHOD__ );
-
-               $update->doUpdate();
-
-               // Prevents: "Notice: DB transaction writes or callbacks still pending"
-               $db2->rollback( __METHOD__ );
-
-               // Ensure it was cancelled
-               $this->assertSame( 0, $ran );
-       }
-
-       public function testCancelAll() {
-               // Prepare update and DB
-               $db1 = new DatabaseTestHelper( __METHOD__ );
-               $db1->begin( __METHOD__ );
-               $db2 = new DatabaseTestHelper( __METHOD__ );
-               $db2->begin( __METHOD__ );
-               $ran = 0;
-               $update = new MWCallableUpdate( function () use ( &$ran ) {
-                       $ran++;
-               }, __METHOD__, [ $db1, $db2 ] );
-
-               // Emulate rollbacks
-               $db1->rollback( __METHOD__ );
-               $db2->rollback( __METHOD__ );
-
-               $update->doUpdate();
-
-               // Ensure it was cancelled
-               $this->assertSame( 0, $ran );
-       }
-
-}
diff --git a/tests/phpunit/includes/deferred/TransactionRoundDefiningUpdateTest.php b/tests/phpunit/includes/deferred/TransactionRoundDefiningUpdateTest.php
deleted file mode 100644 (file)
index 693897e..0000000
+++ /dev/null
@@ -1,19 +0,0 @@
-<?php
-
-/**
- * @covers TransactionRoundDefiningUpdate
- */
-class TransactionRoundDefiningUpdateTest extends PHPUnit\Framework\TestCase {
-
-       use MediaWikiCoversValidator;
-
-       public function testDoUpdate() {
-               $ran = 0;
-               $update = new TransactionRoundDefiningUpdate( function () use ( &$ran ) {
-                       $ran++;
-               } );
-               $this->assertSame( 0, $ran );
-               $update->doUpdate();
-               $this->assertSame( 1, $ran );
-       }
-}
diff --git a/tests/phpunit/includes/diff/ArrayDiffFormatterTest.php b/tests/phpunit/includes/diff/ArrayDiffFormatterTest.php
deleted file mode 100644 (file)
index 8d94404..0000000
+++ /dev/null
@@ -1,134 +0,0 @@
-<?php
-
-/**
- * @author Addshore
- *
- * @group Diff
- */
-class ArrayDiffFormatterTest extends MediaWikiTestCase {
-
-       /**
-        * @param Diff $input
-        * @param array $expectedOutput
-        * @dataProvider provideTestFormat
-        * @covers ArrayDiffFormatter::format
-        */
-       public function testFormat( $input, $expectedOutput ) {
-               $instance = new ArrayDiffFormatter();
-               $output = $instance->format( $input );
-               $this->assertEquals( $expectedOutput, $output );
-       }
-
-       private function getMockDiff( $edits ) {
-               $diff = $this->getMockBuilder( Diff::class )
-                       ->disableOriginalConstructor()
-                       ->getMock();
-               $diff->expects( $this->any() )
-                       ->method( 'getEdits' )
-                       ->will( $this->returnValue( $edits ) );
-               return $diff;
-       }
-
-       private function getMockDiffOp( $type = null, $orig = [], $closing = [] ) {
-               $diffOp = $this->getMockBuilder( DiffOp::class )
-                       ->disableOriginalConstructor()
-                       ->getMock();
-               $diffOp->expects( $this->any() )
-                       ->method( 'getType' )
-                       ->will( $this->returnValue( $type ) );
-               $diffOp->expects( $this->any() )
-                       ->method( 'getOrig' )
-                       ->will( $this->returnValue( $orig ) );
-               if ( $type === 'change' ) {
-                       $diffOp->expects( $this->any() )
-                               ->method( 'getClosing' )
-                               ->with( $this->isType( 'integer' ) )
-                               ->will( $this->returnCallback( function () {
-                                       return 'mockLine';
-                               } ) );
-               } else {
-                       $diffOp->expects( $this->any() )
-                               ->method( 'getClosing' )
-                               ->will( $this->returnValue( $closing ) );
-               }
-               return $diffOp;
-       }
-
-       public function provideTestFormat() {
-               $emptyArrayTestCases = [
-                       $this->getMockDiff( [] ),
-                       $this->getMockDiff( [ $this->getMockDiffOp( 'add' ) ] ),
-                       $this->getMockDiff( [ $this->getMockDiffOp( 'delete' ) ] ),
-                       $this->getMockDiff( [ $this->getMockDiffOp( 'change' ) ] ),
-                       $this->getMockDiff( [ $this->getMockDiffOp( 'copy' ) ] ),
-                       $this->getMockDiff( [ $this->getMockDiffOp( 'FOOBARBAZ' ) ] ),
-                       $this->getMockDiff( [ $this->getMockDiffOp( 'add', 'line' ) ] ),
-                       $this->getMockDiff( [ $this->getMockDiffOp( 'delete', [], [ 'line' ] ) ] ),
-                       $this->getMockDiff( [ $this->getMockDiffOp( 'copy', [], [ 'line' ] ) ] ),
-               ];
-
-               $otherTestCases = [];
-               $otherTestCases[] = [
-                       $this->getMockDiff( [ $this->getMockDiffOp( 'add', [], [ 'a1' ] ) ] ),
-                       [ [ 'action' => 'add', 'new' => 'a1', 'newline' => 1 ] ],
-               ];
-               $otherTestCases[] = [
-                       $this->getMockDiff( [ $this->getMockDiffOp( 'add', [], [ 'a1', 'a2' ] ) ] ),
-                       [
-                               [ 'action' => 'add', 'new' => 'a1', 'newline' => 1 ],
-                               [ 'action' => 'add', 'new' => 'a2', 'newline' => 2 ],
-                       ],
-               ];
-               $otherTestCases[] = [
-                       $this->getMockDiff( [ $this->getMockDiffOp( 'delete', [ 'd1' ] ) ] ),
-                       [ [ 'action' => 'delete', 'old' => 'd1', 'oldline' => 1 ] ],
-               ];
-               $otherTestCases[] = [
-                       $this->getMockDiff( [ $this->getMockDiffOp( 'delete', [ 'd1', 'd2' ] ) ] ),
-                       [
-                               [ 'action' => 'delete', 'old' => 'd1', 'oldline' => 1 ],
-                               [ 'action' => 'delete', 'old' => 'd2', 'oldline' => 2 ],
-                       ],
-               ];
-               $otherTestCases[] = [
-                       $this->getMockDiff( [ $this->getMockDiffOp( 'change', [ 'd1' ], [ 'a1' ] ) ] ),
-                       [ [
-                               'action' => 'change',
-                               'old' => 'd1',
-                               'new' => 'mockLine',
-                               'newline' => 1, 'oldline' => 1
-                       ] ],
-               ];
-               $otherTestCases[] = [
-                       $this->getMockDiff( [ $this->getMockDiffOp(
-                               'change',
-                               [ 'd1', 'd2' ],
-                               [ 'a1', 'a2' ]
-                       ) ] ),
-                       [
-                               [
-                                       'action' => 'change',
-                                       'old' => 'd1',
-                                       'new' => 'mockLine',
-                                       'newline' => 1, 'oldline' => 1
-                               ],
-                               [
-                                       'action' => 'change',
-                                       'old' => 'd2',
-                                       'new' => 'mockLine',
-                                       'newline' => 2, 'oldline' => 2
-                               ],
-                       ],
-               ];
-
-               $testCases = [];
-               foreach ( $emptyArrayTestCases as $testCase ) {
-                       $testCases[] = [ $testCase, [] ];
-               }
-               foreach ( $otherTestCases as $testCase ) {
-                       $testCases[] = [ $testCase[0], $testCase[1] ];
-               }
-               return $testCases;
-       }
-
-}
diff --git a/tests/phpunit/includes/diff/DiffOpTest.php b/tests/phpunit/includes/diff/DiffOpTest.php
deleted file mode 100644 (file)
index 3026fad..0000000
+++ /dev/null
@@ -1,68 +0,0 @@
-<?php
-/**
- * @author Addshore
- *
- * @group Diff
- */
-class DiffOpTest extends MediaWikiTestCase {
-
-       /**
-        * @covers DiffOp::getType
-        */
-       public function testGetType() {
-               $obj = new FakeDiffOp();
-               $obj->type = 'foo';
-               $this->assertEquals( 'foo', $obj->getType() );
-       }
-
-       /**
-        * @covers DiffOp::getOrig
-        */
-       public function testGetOrig() {
-               $obj = new FakeDiffOp();
-               $obj->orig = [ 'foo' ];
-               $this->assertEquals( [ 'foo' ], $obj->getOrig() );
-       }
-
-       /**
-        * @covers DiffOp::getClosing
-        */
-       public function testGetClosing() {
-               $obj = new FakeDiffOp();
-               $obj->closing = [ 'foo' ];
-               $this->assertEquals( [ 'foo' ], $obj->getClosing() );
-       }
-
-       /**
-        * @covers DiffOp::getClosing
-        */
-       public function testGetClosingWithParameter() {
-               $obj = new FakeDiffOp();
-               $obj->closing = [ 'foo', 'bar', 'baz' ];
-               $this->assertEquals( 'foo', $obj->getClosing( 0 ) );
-               $this->assertEquals( 'bar', $obj->getClosing( 1 ) );
-               $this->assertEquals( 'baz', $obj->getClosing( 2 ) );
-               $this->assertEquals( null, $obj->getClosing( 3 ) );
-       }
-
-       /**
-        * @covers DiffOp::norig
-        */
-       public function testNorig() {
-               $obj = new FakeDiffOp();
-               $this->assertEquals( 0, $obj->norig() );
-               $obj->orig = [ 'foo' ];
-               $this->assertEquals( 1, $obj->norig() );
-       }
-
-       /**
-        * @covers DiffOp::nclosing
-        */
-       public function testNclosing() {
-               $obj = new FakeDiffOp();
-               $this->assertEquals( 0, $obj->nclosing() );
-               $obj->closing = [ 'foo' ];
-               $this->assertEquals( 1, $obj->nclosing() );
-       }
-
-}
diff --git a/tests/phpunit/includes/diff/DiffTest.php b/tests/phpunit/includes/diff/DiffTest.php
deleted file mode 100644 (file)
index da6d7d9..0000000
+++ /dev/null
@@ -1,19 +0,0 @@
-<?php
-
-/**
- * @author Addshore
- *
- * @group Diff
- */
-class DiffTest extends MediaWikiTestCase {
-
-       /**
-        * @covers Diff::getEdits
-        */
-       public function testGetEdits() {
-               $obj = new Diff( [], [] );
-               $obj->edits = 'FooBarBaz';
-               $this->assertEquals( 'FooBarBaz', $obj->getEdits() );
-       }
-
-}
diff --git a/tests/phpunit/includes/diff/DifferenceEngineSlotDiffRendererTest.php b/tests/phpunit/includes/diff/DifferenceEngineSlotDiffRendererTest.php
deleted file mode 100644 (file)
index fe129b7..0000000
+++ /dev/null
@@ -1,44 +0,0 @@
-<?php
-
-/**
- * @covers DifferenceEngineSlotDiffRenderer
- */
-class DifferenceEngineSlotDiffRendererTest extends \PHPUnit\Framework\TestCase {
-
-       public function testGetDiff() {
-               $differenceEngine = new CustomDifferenceEngine();
-               $slotDiffRenderer = new DifferenceEngineSlotDiffRenderer( $differenceEngine );
-               $oldContent = ContentHandler::makeContent( 'xxx', null, CONTENT_MODEL_TEXT );
-               $newContent = ContentHandler::makeContent( 'yyy', null, CONTENT_MODEL_TEXT );
-
-               $diff = $slotDiffRenderer->getDiff( $oldContent, $newContent );
-               $this->assertEquals( 'xxx|yyy', $diff );
-
-               $diff = $slotDiffRenderer->getDiff( null, $newContent );
-               $this->assertEquals( '|yyy', $diff );
-
-               $diff = $slotDiffRenderer->getDiff( $oldContent, null );
-               $this->assertEquals( 'xxx|', $diff );
-       }
-
-       public function testAddModules() {
-               $output = $this->getMockBuilder( OutputPage::class )
-                       ->disableOriginalConstructor()
-                       ->setMethods( [ 'addModules' ] )
-                       ->getMock();
-               $output->expects( $this->once() )
-                       ->method( 'addModules' )
-                       ->with( 'foo' );
-               $differenceEngine = new CustomDifferenceEngine();
-               $slotDiffRenderer = new DifferenceEngineSlotDiffRenderer( $differenceEngine );
-               $slotDiffRenderer->addModules( $output );
-       }
-
-       public function testGetExtraCacheKeys() {
-               $differenceEngine = new CustomDifferenceEngine();
-               $slotDiffRenderer = new DifferenceEngineSlotDiffRenderer( $differenceEngine );
-               $extraCacheKeys = $slotDiffRenderer->getExtraCacheKeys();
-               $this->assertSame( [ 'foo' ], $extraCacheKeys );
-       }
-
-}
diff --git a/tests/phpunit/includes/diff/SlotDiffRendererTest.php b/tests/phpunit/includes/diff/SlotDiffRendererTest.php
deleted file mode 100644 (file)
index a03280d..0000000
+++ /dev/null
@@ -1,78 +0,0 @@
-<?php
-
-use Wikimedia\Assert\ParameterTypeException;
-use Wikimedia\TestingAccessWrapper;
-
-/**
- * @covers SlotDiffRenderer
- */
-class SlotDiffRendererTest extends \PHPUnit\Framework\TestCase {
-
-       /**
-        * @dataProvider provideNormalizeContents
-        */
-       public function testNormalizeContents(
-               $oldContent, $newContent, $allowedClasses,
-               $expectedOldContent, $expectedNewContent, $expectedExceptionClass
-       ) {
-               $slotDiffRenderer = $this->getMockBuilder( SlotDiffRenderer::class )
-                       ->getMock();
-               try {
-                       // __call needs help deciding which parameter to take by reference
-                       call_user_func_array( [ TestingAccessWrapper::newFromObject( $slotDiffRenderer ),
-                               'normalizeContents' ], [ &$oldContent, &$newContent, $allowedClasses ] );
-                       $this->assertEquals( $expectedOldContent, $oldContent );
-                       $this->assertEquals( $expectedNewContent, $newContent );
-               } catch ( Exception $e ) {
-                       if ( !$expectedExceptionClass ) {
-                               throw $e;
-                       }
-                       $this->assertInstanceOf( $expectedExceptionClass, $e );
-               }
-       }
-
-       public function provideNormalizeContents() {
-               return [
-                       'both null' => [ null, null, null, null, null, InvalidArgumentException::class ],
-                       'left null' => [
-                               null, new WikitextContent( 'abc' ), null,
-                               new WikitextContent( '' ), new WikitextContent( 'abc' ), null,
-                       ],
-                       'right null' => [
-                               new WikitextContent( 'def' ), null, null,
-                               new WikitextContent( 'def' ), new WikitextContent( '' ), null,
-                       ],
-                       'type filter' => [
-                               new WikitextContent( 'abc' ), new WikitextContent( 'def' ), WikitextContent::class,
-                               new WikitextContent( 'abc' ), new WikitextContent( 'def' ), null,
-                       ],
-                       'type filter (subclass)' => [
-                               new WikitextContent( 'abc' ), new WikitextContent( 'def' ), TextContent::class,
-                               new WikitextContent( 'abc' ), new WikitextContent( 'def' ), null,
-                       ],
-                       'type filter (null)' => [
-                               new WikitextContent( 'abc' ), null, TextContent::class,
-                               new WikitextContent( 'abc' ), new WikitextContent( '' ), null,
-                       ],
-                       'type filter failure (left)' => [
-                               new TextContent( 'abc' ), new WikitextContent( 'def' ), WikitextContent::class,
-                               null, null, ParameterTypeException::class,
-                       ],
-                       'type filter failure (right)' => [
-                               new WikitextContent( 'abc' ), new TextContent( 'def' ), WikitextContent::class,
-                               null, null, ParameterTypeException::class,
-                       ],
-                       'type filter (array syntax)' => [
-                               new WikitextContent( 'abc' ), new JsonContent( 'def' ),
-                               [ JsonContent::class, WikitextContent::class ],
-                               new WikitextContent( 'abc' ), new JsonContent( 'def' ), null,
-                       ],
-                       'type filter failure (array syntax)' => [
-                               new WikitextContent( 'abc' ), new CssContent( 'def' ),
-                               [ JsonContent::class, WikitextContent::class ],
-                               null, null, ParameterTypeException::class,
-                       ],
-               ];
-       }
-
-}
diff --git a/tests/phpunit/includes/exception/HttpErrorTest.php b/tests/phpunit/includes/exception/HttpErrorTest.php
deleted file mode 100644 (file)
index 90ccd1e..0000000
+++ /dev/null
@@ -1,63 +0,0 @@
-<?php
-
-/**
- * @todo tests for HttpError::report
- *
- * @covers HttpError
- */
-class HttpErrorTest extends MediaWikiTestCase {
-
-       public function testIsLoggable() {
-               $httpError = new HttpError( 500, 'server error!' );
-               $this->assertFalse( $httpError->isLoggable(), 'http error is not loggable' );
-       }
-
-       public function testGetStatusCode() {
-               $httpError = new HttpError( 500, 'server error!' );
-               $this->assertEquals( 500, $httpError->getStatusCode() );
-       }
-
-       /**
-        * @dataProvider getHtmlProvider
-        */
-       public function testGetHtml( array $expected, $content, $header ) {
-               $httpError = new HttpError( 500, $content, $header );
-               $errorHtml = $httpError->getHTML();
-
-               foreach ( $expected as $key => $html ) {
-                       $this->assertContains( $html, $errorHtml, $key );
-               }
-       }
-
-       public function getHtmlProvider() {
-               return [
-                       [
-                               [
-                                       'head html' => '<head><title>Server Error 123</title></head>',
-                                       'body html' => '<body><h1>Server Error 123</h1>'
-                                               . '<p>a server error!</p></body>'
-                               ],
-                               'a server error!',
-                               'Server Error 123'
-                       ],
-                       [
-                               [
-                                       'head html' => '<head><title>loginerror</title></head>',
-                                       'body html' => '<body><h1>loginerror</h1>'
-                                       . '<p>suspicious-userlogout</p></body>'
-                               ],
-                               new RawMessage( 'suspicious-userlogout' ),
-                               new RawMessage( 'loginerror' )
-                       ],
-                       [
-                               [
-                                       'head html' => '<html><head><title>Internal Server Error</title></head>',
-                                       'body html' => '<body><h1>Internal Server Error</h1>'
-                                               . '<p>a server error!</p></body></html>'
-                               ],
-                               'a server error!',
-                               null
-                       ]
-               ];
-       }
-}
diff --git a/tests/phpunit/includes/exception/MWExceptionHandlerTest.php b/tests/phpunit/includes/exception/MWExceptionHandlerTest.php
deleted file mode 100644 (file)
index 6606065..0000000
+++ /dev/null
@@ -1,74 +0,0 @@
-<?php
-/**
- * @author Antoine Musso
- * @copyright Copyright © 2013, Antoine Musso
- * @copyright Copyright © 2013, Wikimedia Foundation Inc.
- * @file
- */
-
-class MWExceptionHandlerTest extends MediaWikiTestCase {
-
-       /**
-        * @covers MWExceptionHandler::getRedactedTrace
-        */
-       public function testGetRedactedTrace() {
-               $refvar = 'value';
-               try {
-                       $array = [ 'a', 'b' ];
-                       $object = new stdClass();
-                       self::helperThrowAnException( $array, $object, $refvar );
-               } catch ( Exception $e ) {
-               }
-
-               # Make sure our stack trace contains an array and an object passed to
-               # some function in the stacktrace. Else, we can not assert the trace
-               # redaction achieved its job.
-               $trace = $e->getTrace();
-               $hasObject = false;
-               $hasArray = false;
-               foreach ( $trace as $frame ) {
-                       if ( !isset( $frame['args'] ) ) {
-                               continue;
-                       }
-                       foreach ( $frame['args'] as $arg ) {
-                               $hasObject = $hasObject || is_object( $arg );
-                               $hasArray = $hasArray || is_array( $arg );
-                       }
-
-                       if ( $hasObject && $hasArray ) {
-                               break;
-                       }
-               }
-               $this->assertTrue( $hasObject,
-                       "The stacktrace must have a function having an object has parameter" );
-               $this->assertTrue( $hasArray,
-                       "The stacktrace must have a function having an array has parameter" );
-
-               # Now we redact the trace.. and make sure no function arguments are
-               # arrays or objects.
-               $redacted = MWExceptionHandler::getRedactedTrace( $e );
-
-               foreach ( $redacted as $frame ) {
-                       if ( !isset( $frame['args'] ) ) {
-                               continue;
-                       }
-                       foreach ( $frame['args'] as $arg ) {
-                               $this->assertNotInternalType( 'array', $arg );
-                               $this->assertNotInternalType( 'object', $arg );
-                       }
-               }
-
-               $this->assertEquals( 'value', $refvar, 'Ensuring reference variable wasn\'t changed' );
-       }
-
-       /**
-        * Helper function for testExpandArgumentsInCall
-        *
-        * Pass it an object and an array, and something by reference :-)
-        *
-        * @throws Exception
-        */
-       protected static function helperThrowAnException( $a, $b, &$c ) {
-               throw new Exception();
-       }
-}
diff --git a/tests/phpunit/includes/exception/ReadOnlyErrorTest.php b/tests/phpunit/includes/exception/ReadOnlyErrorTest.php
deleted file mode 100644 (file)
index ee5becf..0000000
+++ /dev/null
@@ -1,16 +0,0 @@
-<?php
-
-/**
- * @covers ReadOnlyError
- * @author Addshore
- */
-class ReadOnlyErrorTest extends MediaWikiTestCase {
-
-       public function testConstruction() {
-               $e = new ReadOnlyError();
-               $this->assertEquals( 'readonly', $e->title );
-               $this->assertEquals( 'readonlytext', $e->msg );
-               $this->assertEquals( wfReadOnlyReason() ?: [], $e->params );
-       }
-
-}
diff --git a/tests/phpunit/includes/exception/UserNotLoggedInTest.php b/tests/phpunit/includes/exception/UserNotLoggedInTest.php
deleted file mode 100644 (file)
index 55ec45a..0000000
+++ /dev/null
@@ -1,16 +0,0 @@
-<?php
-
-/**
- * @covers UserNotLoggedIn
- * @author Addshore
- */
-class UserNotLoggedInTest extends MediaWikiTestCase {
-
-       public function testConstruction() {
-               $e = new UserNotLoggedIn();
-               $this->assertEquals( 'exception-nologin', $e->title );
-               $this->assertEquals( 'exception-nologin-text', $e->msg );
-               $this->assertEquals( [], $e->params );
-       }
-
-}
diff --git a/tests/phpunit/includes/externalstore/ExternalStoreFactoryTest.php b/tests/phpunit/includes/externalstore/ExternalStoreFactoryTest.php
deleted file mode 100644 (file)
index f762693..0000000
+++ /dev/null
@@ -1,41 +0,0 @@
-<?php
-
-/**
- * @covers ExternalStoreFactory
- */
-class ExternalStoreFactoryTest extends PHPUnit\Framework\TestCase {
-
-       use MediaWikiCoversValidator;
-
-       public function testExternalStoreFactory_noStores() {
-               $factory = new ExternalStoreFactory( [] );
-               $this->assertFalse( $factory->getStoreObject( 'ForTesting' ) );
-               $this->assertFalse( $factory->getStoreObject( 'foo' ) );
-       }
-
-       public function provideStoreNames() {
-               yield 'Same case as construction' => [ 'ForTesting' ];
-               yield 'All lower case' => [ 'fortesting' ];
-               yield 'All upper case' => [ 'FORTESTING' ];
-               yield 'Mix of cases' => [ 'FOrTEsTInG' ];
-       }
-
-       /**
-        * @dataProvider provideStoreNames
-        */
-       public function testExternalStoreFactory_someStore_protoMatch( $proto ) {
-               $factory = new ExternalStoreFactory( [ 'ForTesting' ] );
-               $store = $factory->getStoreObject( $proto );
-               $this->assertInstanceOf( ExternalStoreForTesting::class, $store );
-       }
-
-       /**
-        * @dataProvider provideStoreNames
-        */
-       public function testExternalStoreFactory_someStore_noProtoMatch( $proto ) {
-               $factory = new ExternalStoreFactory( [ 'SomeOtherClassName' ] );
-               $store = $factory->getStoreObject( $proto );
-               $this->assertFalse( $store );
-       }
-
-}
diff --git a/tests/phpunit/includes/filebackend/SwiftFileBackendTest.php b/tests/phpunit/includes/filebackend/SwiftFileBackendTest.php
deleted file mode 100644 (file)
index 35eca28..0000000
+++ /dev/null
@@ -1,216 +0,0 @@
-<?php
-
-use Wikimedia\TestingAccessWrapper;
-
-/**
- * @group FileRepo
- * @group FileBackend
- * @group medium
- *
- * @covers SwiftFileBackend
- * @covers SwiftFileBackendDirList
- * @covers SwiftFileBackendFileList
- * @covers SwiftFileBackendList
- */
-class SwiftFileBackendTest extends MediaWikiTestCase {
-       /** @var TestingAccessWrapper Proxy to SwiftFileBackend */
-       private $backend;
-
-       protected function setUp() {
-               parent::setUp();
-
-               $this->backend = TestingAccessWrapper::newFromObject(
-                       new SwiftFileBackend( [
-                               'name'             => 'local-swift-testing',
-                               'class'            => SwiftFileBackend::class,
-                               'wikiId'           => 'unit-testing',
-                               'lockManager'      => LockManagerGroup::singleton()->get( 'fsLockManager' ),
-                               'swiftAuthUrl'     => 'http://127.0.0.1:8080/auth', // unused
-                               'swiftUser'        => 'test:tester',
-                               'swiftKey'         => 'testing',
-                               'swiftTempUrlKey'  => 'b3968d0207b54ece87cccc06515a89d4' // unused
-                       ] )
-               );
-       }
-
-       /**
-        * @dataProvider provider_testSanitizeHdrsStrict
-        */
-       public function testSanitizeHdrsStrict( $raw, $sanitized ) {
-               $hdrs = $this->backend->sanitizeHdrsStrict( [ 'headers' => $raw ] );
-
-               $this->assertEquals( $hdrs, $sanitized, 'sanitizeHdrsStrict() has expected result' );
-       }
-
-       public static function provider_testSanitizeHdrsStrict() {
-               return [
-                       [
-                               [
-                                       'content-length' => 345,
-                                       'content-type'   => 'image+bitmap/jpeg',
-                                       'content-disposition' => 'inline',
-                                       'content-duration' => 35.6363,
-                                       'content-Custom' => 'hello',
-                                       'x-content-custom' => 'hello'
-                               ],
-                               [
-                                       'content-disposition' => 'inline',
-                                       'content-duration' => 35.6363,
-                                       'content-custom' => 'hello',
-                                       'x-content-custom' => 'hello'
-                               ]
-                       ],
-                       [
-                               [
-                                       'content-length' => 345,
-                                       'content-type'   => 'image+bitmap/jpeg',
-                                       'content-Disposition' => 'inline; filename=xxx; ' . str_repeat( 'o', 1024 ),
-                                       'content-duration' => 35.6363,
-                                       'content-custom' => 'hello',
-                                       'x-content-custom' => 'hello'
-                               ],
-                               [
-                                       'content-disposition' => 'inline;filename=xxx',
-                                       'content-duration' => 35.6363,
-                                       'content-custom' => 'hello',
-                                       'x-content-custom' => 'hello'
-                               ]
-                       ],
-                       [
-                               [
-                                       'content-length' => 345,
-                                       'content-type'   => 'image+bitmap/jpeg',
-                                       'content-disposition' => 'filename=' . str_repeat( 'o', 1024 ) . ';inline',
-                                       'content-duration' => 35.6363,
-                                       'content-custom' => 'hello',
-                                       'x-content-custom' => 'hello'
-                               ],
-                               [
-                                       'content-disposition' => '',
-                                       'content-duration' => 35.6363,
-                                       'content-custom' => 'hello',
-                                       'x-content-custom' => 'hello'
-                               ]
-                       ]
-               ];
-       }
-
-       /**
-        * @dataProvider provider_testSanitizeHdrs
-        */
-       public function testSanitizeHdrs( $raw, $sanitized ) {
-               $hdrs = $this->backend->sanitizeHdrs( [ 'headers' => $raw ] );
-
-               $this->assertEquals( $hdrs, $sanitized, 'sanitizeHdrs() has expected result' );
-       }
-
-       public static function provider_testSanitizeHdrs() {
-               return [
-                       [
-                               [
-                                       'content-length' => 345,
-                                       'content-type'   => 'image+bitmap/jpeg',
-                                       'content-disposition' => 'inline',
-                                       'content-duration' => 35.6363,
-                                       'content-Custom' => 'hello',
-                                       'x-content-custom' => 'hello'
-                               ],
-                               [
-                                       'content-type'   => 'image+bitmap/jpeg',
-                                       'content-disposition' => 'inline',
-                                       'content-duration' => 35.6363,
-                                       'content-custom' => 'hello',
-                                       'x-content-custom' => 'hello'
-                               ]
-                       ],
-                       [
-                               [
-                                       'content-length' => 345,
-                                       'content-type'   => 'image+bitmap/jpeg',
-                                       'content-Disposition' => 'inline; filename=xxx; ' . str_repeat( 'o', 1024 ),
-                                       'content-duration' => 35.6363,
-                                       'content-custom' => 'hello',
-                                       'x-content-custom' => 'hello'
-                               ],
-                               [
-                                       'content-type'   => 'image+bitmap/jpeg',
-                                       'content-disposition' => 'inline;filename=xxx',
-                                       'content-duration' => 35.6363,
-                                       'content-custom' => 'hello',
-                                       'x-content-custom' => 'hello'
-                               ]
-                       ],
-                       [
-                               [
-                                       'content-length' => 345,
-                                       'content-type'   => 'image+bitmap/jpeg',
-                                       'content-disposition' => 'filename=' . str_repeat( 'o', 1024 ) . ';inline',
-                                       'content-duration' => 35.6363,
-                                       'content-custom' => 'hello',
-                                       'x-content-custom' => 'hello'
-                               ],
-                               [
-                                       'content-type'   => 'image+bitmap/jpeg',
-                                       'content-disposition' => '',
-                                       'content-duration' => 35.6363,
-                                       'content-custom' => 'hello',
-                                       'x-content-custom' => 'hello'
-                               ]
-                       ]
-               ];
-       }
-
-       /**
-        * @dataProvider provider_testGetMetadataHeaders
-        */
-       public function testGetMetadataHeaders( $raw, $sanitized ) {
-               $hdrs = $this->backend->getMetadataHeaders( $raw );
-
-               $this->assertEquals( $hdrs, $sanitized, 'getMetadataHeaders() has expected result' );
-       }
-
-       public static function provider_testGetMetadataHeaders() {
-               return [
-                       [
-                               [
-                                       'content-length' => 345,
-                                       'content-custom' => 'hello',
-                                       'x-content-custom' => 'hello',
-                                       'x-object-meta-custom' => 5,
-                                       'x-object-meta-sha1Base36' => 'a3deadfg...',
-                               ],
-                               [
-                                       'x-object-meta-custom' => 5,
-                                       'x-object-meta-sha1base36' => 'a3deadfg...',
-                               ]
-                       ]
-               ];
-       }
-
-       /**
-        * @dataProvider provider_testGetMetadata
-        */
-       public function testGetMetadata( $raw, $sanitized ) {
-               $hdrs = $this->backend->getMetadata( $raw );
-
-               $this->assertEquals( $hdrs, $sanitized, 'getMetadata() has expected result' );
-       }
-
-       public static function provider_testGetMetadata() {
-               return [
-                       [
-                               [
-                                       'content-length' => 345,
-                                       'content-custom' => 'hello',
-                                       'x-content-custom' => 'hello',
-                                       'x-object-meta-custom' => 5,
-                                       'x-object-meta-sha1Base36' => 'a3deadfg...',
-                               ],
-                               [
-                                       'custom' => 5,
-                                       'sha1base36' => 'a3deadfg...',
-                               ]
-                       ]
-               ];
-       }
-}
diff --git a/tests/phpunit/includes/filerepo/FileBackendDBRepoWrapperTest.php b/tests/phpunit/includes/filerepo/FileBackendDBRepoWrapperTest.php
deleted file mode 100644 (file)
index 346be7a..0000000
+++ /dev/null
@@ -1,140 +0,0 @@
-<?php
-
-class FileBackendDBRepoWrapperTest extends MediaWikiTestCase {
-       protected $backendName = 'foo-backend';
-       protected $repoName = 'pureTestRepo';
-
-       /**
-        * @dataProvider getBackendPathsProvider
-        * @covers FileBackendDBRepoWrapper::getBackendPaths
-        */
-       public function testGetBackendPaths(
-               $mocks,
-               $latest,
-               $dbReadsExpected,
-               $dbReturnValue,
-               $originalPath,
-               $expectedBackendPath,
-               $message ) {
-               list( $dbMock, $backendMock, $wrapperMock ) = $mocks;
-
-               $dbMock->expects( $dbReadsExpected )
-                       ->method( 'selectField' )
-                       ->will( $this->returnValue( $dbReturnValue ) );
-
-               $newPaths = $wrapperMock->getBackendPaths( [ $originalPath ], $latest );
-
-               $this->assertEquals(
-                       $expectedBackendPath,
-                       $newPaths[0],
-                       $message );
-       }
-
-       public function getBackendPathsProvider() {
-               $prefix = 'mwstore://' . $this->backendName . '/' . $this->repoName;
-               $mocksForCaching = $this->getMocks();
-
-               return [
-                       [
-                               $mocksForCaching,
-                               false,
-                               $this->once(),
-                               '96246614d75ba1703bdfd5d7660bb57407aaf5d9',
-                               $prefix . '-public/f/o/foobar.jpg',
-                               $prefix . '-original/9/6/2/96246614d75ba1703bdfd5d7660bb57407aaf5d9',
-                               'Public path translated correctly',
-                       ],
-                       [
-                               $mocksForCaching,
-                               false,
-                               $this->never(),
-                               '96246614d75ba1703bdfd5d7660bb57407aaf5d9',
-                               $prefix . '-public/f/o/foobar.jpg',
-                               $prefix . '-original/9/6/2/96246614d75ba1703bdfd5d7660bb57407aaf5d9',
-                               'LRU cache leveraged',
-                       ],
-                       [
-                               $this->getMocks(),
-                               true,
-                               $this->once(),
-                               '96246614d75ba1703bdfd5d7660bb57407aaf5d9',
-                               $prefix . '-public/f/o/foobar.jpg',
-                               $prefix . '-original/9/6/2/96246614d75ba1703bdfd5d7660bb57407aaf5d9',
-                               'Latest obtained',
-                       ],
-                       [
-                               $this->getMocks(),
-                               true,
-                               $this->never(),
-                               '96246614d75ba1703bdfd5d7660bb57407aaf5d9',
-                               $prefix . '-deleted/f/o/foobar.jpg',
-                               $prefix . '-original/f/o/o/foobar',
-                               'Deleted path translated correctly',
-                       ],
-                       [
-                               $this->getMocks(),
-                               true,
-                               $this->once(),
-                               null,
-                               $prefix . '-public/b/a/baz.jpg',
-                               $prefix . '-public/b/a/baz.jpg',
-                               'Path left untouched if no sha1 can be found',
-                       ],
-               ];
-       }
-
-       /**
-        * @covers FileBackendDBRepoWrapper::getFileContentsMulti
-        */
-       public function testGetFileContentsMulti() {
-               list( $dbMock, $backendMock, $wrapperMock ) = $this->getMocks();
-
-               $sha1Path = 'mwstore://' . $this->backendName . '/' . $this->repoName
-                       . '-original/9/6/2/96246614d75ba1703bdfd5d7660bb57407aaf5d9';
-               $filenamePath = 'mwstore://' . $this->backendName . '/' . $this->repoName
-                       . '-public/f/o/foobar.jpg';
-
-               $dbMock->expects( $this->once() )
-                       ->method( 'selectField' )
-                       ->will( $this->returnValue( '96246614d75ba1703bdfd5d7660bb57407aaf5d9' ) );
-
-               $backendMock->expects( $this->once() )
-                       ->method( 'getFileContentsMulti' )
-                       ->will( $this->returnValue( [ $sha1Path => 'foo' ] ) );
-
-               $result = $wrapperMock->getFileContentsMulti( [ 'srcs' => [ $filenamePath ] ] );
-
-               $this->assertEquals(
-                       [ $filenamePath => 'foo' ],
-                       $result,
-                       'File contents paths translated properly'
-               );
-       }
-
-       protected function getMocks() {
-               $dbMock = $this->getMockBuilder( Wikimedia\Rdbms\IDatabase::class )
-                       ->disableOriginalClone()
-                       ->disableOriginalConstructor()
-                       ->getMock();
-
-               $backendMock = $this->getMockBuilder( FSFileBackend::class )
-                       ->setConstructorArgs( [ [
-                                       'name' => $this->backendName,
-                                       'wikiId' => wfWikiID()
-                               ] ] )
-                       ->getMock();
-
-               $wrapperMock = $this->getMockBuilder( FileBackendDBRepoWrapper::class )
-                       ->setMethods( [ 'getDB' ] )
-                       ->setConstructorArgs( [ [
-                                       'backend' => $backendMock,
-                                       'repoName' => $this->repoName,
-                                       'dbHandleFactory' => null
-                               ] ] )
-                       ->getMock();
-
-               $wrapperMock->expects( $this->any() )->method( 'getDB' )->will( $this->returnValue( $dbMock ) );
-
-               return [ $dbMock, $backendMock, $wrapperMock ];
-       }
-}
diff --git a/tests/phpunit/includes/filerepo/FileRepoTest.php b/tests/phpunit/includes/filerepo/FileRepoTest.php
deleted file mode 100644 (file)
index 0d3e679..0000000
+++ /dev/null
@@ -1,55 +0,0 @@
-<?php
-
-class FileRepoTest extends MediaWikiTestCase {
-
-       /**
-        * @expectedException MWException
-        * @covers FileRepo::__construct
-        */
-       public function testFileRepoConstructionOptionCanNotBeNull() {
-               new FileRepo();
-       }
-
-       /**
-        * @expectedException MWException
-        * @covers FileRepo::__construct
-        */
-       public function testFileRepoConstructionOptionCanNotBeAnEmptyArray() {
-               new FileRepo( [] );
-       }
-
-       /**
-        * @expectedException MWException
-        * @covers FileRepo::__construct
-        */
-       public function testFileRepoConstructionOptionNeedNameKey() {
-               new FileRepo( [
-                       'backend' => 'foobar'
-               ] );
-       }
-
-       /**
-        * @expectedException MWException
-        * @covers FileRepo::__construct
-        */
-       public function testFileRepoConstructionOptionNeedBackendKey() {
-               new FileRepo( [
-                       'name' => 'foobar'
-               ] );
-       }
-
-       /**
-        * @covers FileRepo::__construct
-        */
-       public function testFileRepoConstructionWithRequiredOptions() {
-               $f = new FileRepo( [
-                       'name' => 'FileRepoTestRepository',
-                       'backend' => new FSFileBackend( [
-                               'name' => 'local-testing',
-                               'wikiId' => 'test_wiki',
-                               'containerPaths' => []
-                       ] )
-               ] );
-               $this->assertInstanceOf( FileRepo::class, $f );
-       }
-}
diff --git a/tests/phpunit/includes/htmlform/HTMLAutoCompleteSelectFieldTest.php b/tests/phpunit/includes/htmlform/HTMLAutoCompleteSelectFieldTest.php
deleted file mode 100644 (file)
index eaba22d..0000000
+++ /dev/null
@@ -1,66 +0,0 @@
-<?php
-/**
- * @covers HTMLAutoCompleteSelectField
- */
-class HTMLAutoCompleteSelectFieldTest extends MediaWikiTestCase {
-
-       public $options = [
-               'Bulgaria'     => 'BGR',
-               'Burkina Faso' => 'BFA',
-               'Burundi'      => 'BDI',
-       ];
-
-       /**
-        * Verify that attempting to instantiate an HTMLAutoCompleteSelectField
-        * without providing any autocomplete options causes an exception to be
-        * thrown.
-        *
-        * @expectedException        MWException
-        * @expectedExceptionMessage called without any autocompletions
-        */
-       function testMissingAutocompletions() {
-               new HTMLAutoCompleteSelectField( [ 'fieldname' => 'Test' ] );
-       }
-
-       /**
-        * Verify that the autocomplete options are correctly encoded as
-        * the 'data-autocomplete' attribute of the field.
-        *
-        * @covers HTMLAutoCompleteSelectField::getAttributes
-        */
-       function testGetAttributes() {
-               $field = new HTMLAutoCompleteSelectField( [
-                       'fieldname'    => 'Test',
-                       'autocomplete' => $this->options,
-               ] );
-
-               $attributes = $field->getAttributes( [] );
-               $this->assertEquals( array_keys( $this->options ),
-                       FormatJson::decode( $attributes['data-autocomplete'] ),
-                       "The 'data-autocomplete' attribute encodes autocomplete option keys as a JSON array."
-               );
-       }
-
-       /**
-        * Test that the optional select dropdown is included or excluded based on
-        * the presence or absence of the 'options' parameter.
-        */
-       function testOptionalSelectElement() {
-               $params = [
-                       'fieldname'         => 'Test',
-                       'autocomplete-data' => $this->options,
-                       'options'           => $this->options,
-               ];
-
-               $field = new HTMLAutoCompleteSelectField( $params );
-               $html = $field->getInputHTML( false );
-               $this->assertRegExp( '/select/', $html,
-                       "When the 'options' parameter is set, the HTML includes a <select>" );
-
-               unset( $params['options'] );
-               $field = new HTMLAutoCompleteSelectField( $params );
-               $html = $field->getInputHTML( false );
-               $this->assertNotRegExp( '/select/', $html,
-                       "When the 'options' parameter is not set, the HTML does not include a <select>" );
-       }
-}
diff --git a/tests/phpunit/includes/htmlform/HTMLCheckMatrixTest.php b/tests/phpunit/includes/htmlform/HTMLCheckMatrixTest.php
deleted file mode 100644 (file)
index 05c567d..0000000
+++ /dev/null
@@ -1,104 +0,0 @@
-<?php
-
-/**
- * @covers HTMLCheckMatrix
- */
-class HTMLCheckMatrixTest extends MediaWikiTestCase {
-       private static $defaultOptions = [
-               'rows' => [ 'r1', 'r2' ],
-               'columns' => [ 'c1', 'c2' ],
-               'fieldname' => 'test',
-       ];
-
-       public function testPlainInstantiation() {
-               try {
-                       new HTMLCheckMatrix( [] );
-               } catch ( MWException $e ) {
-                       $this->assertInstanceOf( HTMLFormFieldRequiredOptionsException::class, $e );
-                       return;
-               }
-
-               $this->fail( 'Expected MWException indicating missing parameters but none was thrown.' );
-       }
-
-       public function testInstantiationWithMinimumRequiredParameters() {
-               new HTMLCheckMatrix( self::$defaultOptions );
-               $this->assertTrue( true ); // form instantiation must throw exception on failure
-       }
-
-       public function testValidateCallsUserDefinedValidationCallback() {
-               $called = false;
-               $field = new HTMLCheckMatrix( self::$defaultOptions + [
-                       'validation-callback' => function () use ( &$called ) {
-                               $called = true;
-
-                               return false;
-                       },
-               ] );
-               $this->assertEquals( false, $this->validate( $field, [] ) );
-               $this->assertTrue( $called );
-       }
-
-       public function testValidateRequiresArrayInput() {
-               $field = new HTMLCheckMatrix( self::$defaultOptions );
-               $this->assertEquals( false, $this->validate( $field, null ) );
-               $this->assertEquals( false, $this->validate( $field, true ) );
-               $this->assertEquals( false, $this->validate( $field, 'abc' ) );
-               $this->assertEquals( false, $this->validate( $field, new stdClass ) );
-               $this->assertEquals( true, $this->validate( $field, [] ) );
-       }
-
-       public function testValidateAllowsOnlyKnownTags() {
-               $field = new HTMLCheckMatrix( self::$defaultOptions );
-               $this->assertInstanceOf( Message::class, $this->validate( $field, [ 'foo' ] ) );
-       }
-
-       public function testValidateAcceptsPartialTagList() {
-               $field = new HTMLCheckMatrix( self::$defaultOptions );
-               $this->assertTrue( $this->validate( $field, [] ) );
-               $this->assertTrue( $this->validate( $field, [ 'c1-r1' ] ) );
-               $this->assertTrue( $this->validate( $field, [ 'c1-r1', 'c1-r2', 'c2-r1', 'c2-r2' ] ) );
-       }
-
-       /**
-        * This form object actually has no visibility into what happens later on, but essentially
-        * if the data submitted by the user passes validate the following is run:
-        * foreach ( $field->filterDataForSubmit( $data ) as $k => $v ) {
-        *     $user->setOption( $k, $v );
-        * }
-        */
-       public function testValuesForcedOnRemainOn() {
-               $field = new HTMLCheckMatrix( self::$defaultOptions + [
-                               'force-options-on' => [ 'c2-r1' ],
-                       ] );
-               $expected = [
-                       'c1-r1' => false,
-                       'c1-r2' => false,
-                       'c2-r1' => true,
-                       'c2-r2' => false,
-               ];
-               $this->assertEquals( $expected, $field->filterDataForSubmit( [] ) );
-       }
-
-       public function testValuesForcedOffRemainOff() {
-               $field = new HTMLCheckMatrix( self::$defaultOptions + [
-                               'force-options-off' => [ 'c1-r2', 'c2-r2' ],
-                       ] );
-               $expected = [
-                       'c1-r1' => true,
-                       'c1-r2' => false,
-                       'c2-r1' => true,
-                       'c2-r2' => false,
-               ];
-               // array_keys on the result simulates submitting all fields checked
-               $this->assertEquals( $expected, $field->filterDataForSubmit( array_keys( $expected ) ) );
-       }
-
-       protected function validate( HTMLFormField $field, $submitted ) {
-               return $field->validate(
-                       $submitted,
-                       [ self::$defaultOptions['fieldname'] => $submitted ]
-               );
-       }
-
-}
diff --git a/tests/phpunit/includes/htmlform/HTMLFormTest.php b/tests/phpunit/includes/htmlform/HTMLFormTest.php
deleted file mode 100644 (file)
index d7dc411..0000000
+++ /dev/null
@@ -1,63 +0,0 @@
-<?php
-
-/**
- * @covers HTMLForm
- *
- * @license GPL-2.0-or-later
- * @author Gergő Tisza
- */
-class HTMLFormTest extends MediaWikiTestCase {
-
-       private function newInstance() {
-               $form = new HTMLForm( [] );
-               $form->setTitle( Title::newFromText( 'Foo' ) );
-               return $form;
-       }
-
-       public function testGetHTML_empty() {
-               $form = $this->newInstance();
-               $form->prepareForm();
-               $html = $form->getHTML( false );
-               $this->assertStringStartsWith( '<form ', $html );
-       }
-
-       /**
-        * @expectedException LogicException
-        */
-       public function testGetHTML_noPrepare() {
-               $form = $this->newInstance();
-               $form->getHTML( false );
-       }
-
-       public function testAutocompleteDefaultsToNull() {
-               $form = $this->newInstance();
-               $this->assertNotContains( 'autocomplete', $form->wrapForm( '' ) );
-       }
-
-       public function testAutocompleteWhenSetToNull() {
-               $form = $this->newInstance();
-               $form->setAutocomplete( null );
-               $this->assertNotContains( 'autocomplete', $form->wrapForm( '' ) );
-       }
-
-       public function testAutocompleteWhenSetToFalse() {
-               $form = $this->newInstance();
-               // Previously false was used instead of null to indicate the attribute should not be set
-               $form->setAutocomplete( false );
-               $this->assertNotContains( 'autocomplete', $form->wrapForm( '' ) );
-       }
-
-       public function testAutocompleteWhenSetToOff() {
-               $form = $this->newInstance();
-               $form->setAutocomplete( 'off' );
-               $this->assertContains( ' autocomplete="off"', $form->wrapForm( '' ) );
-       }
-
-       public function testGetPreText() {
-               $preText = 'TEST';
-               $form = $this->newInstance();
-               $form->setPreText( $preText );
-               $this->assertSame( $preText, $form->getPreText() );
-       }
-
-}
diff --git a/tests/phpunit/includes/htmlform/HTMLRestrictionsFieldTest.php b/tests/phpunit/includes/htmlform/HTMLRestrictionsFieldTest.php
deleted file mode 100644 (file)
index c4290e1..0000000
+++ /dev/null
@@ -1,71 +0,0 @@
-<?php
-
-/**
- * @covers HTMLRestrictionsField
- */
-class HTMLRestrictionsFieldTest extends PHPUnit\Framework\TestCase {
-
-       use MediaWikiCoversValidator;
-
-       public function testConstruct() {
-               $field = new HTMLRestrictionsField( [ 'fieldname' => 'restrictions' ] );
-               $this->assertNotEmpty( $field->getLabel(), 'has a default label' );
-               $this->assertNotEmpty( $field->getHelpText(), 'has a default help text' );
-               $this->assertEquals( MWRestrictions::newDefault(), $field->getDefault(),
-                       'defaults to the default MWRestrictions object' );
-
-               $field = new HTMLRestrictionsField( [
-                       'fieldname' => 'restrictions',
-                       'label' => 'foo',
-                       'help' => 'bar',
-                       'default' => 'baz',
-               ] );
-               $this->assertEquals( 'foo', $field->getLabel(), 'label can be customized' );
-               $this->assertEquals( 'bar', $field->getHelpText(), 'help text can be customized' );
-               $this->assertEquals( 'baz', $field->getDefault(), 'default can be customized' );
-       }
-
-       /**
-        * @dataProvider provideValidate
-        */
-       public function testForm( $text, $value ) {
-               $form = HTMLForm::factory( 'ooui', [
-                       'restrictions' => [ 'class' => HTMLRestrictionsField::class ],
-               ] );
-               $request = new FauxRequest( [ 'wprestrictions' => $text ], true );
-               $context = new DerivativeContext( RequestContext::getMain() );
-               $context->setRequest( $request );
-               $form->setContext( $context );
-               $form->setTitle( Title::newFromText( 'Main Page' ) )->setSubmitCallback( function () {
-                       return true;
-               } )->prepareForm();
-               $status = $form->trySubmit();
-
-               if ( $status instanceof StatusValue ) {
-                       $this->assertEquals( $value !== false, $status->isGood() );
-               } elseif ( $value === false ) {
-                       $this->assertNotSame( true, $status );
-               } else {
-                       $this->assertSame( true, $status );
-               }
-
-               if ( $value !== false ) {
-                       $restrictions = $form->mFieldData['restrictions'];
-                       $this->assertInstanceOf( MWRestrictions::class, $restrictions );
-                       $this->assertEquals( $value, $restrictions->toArray()['IPAddresses'] );
-               }
-
-               // sanity
-               $form->getHTML( $status );
-       }
-
-       public function provideValidate() {
-               return [
-                       // submitted text, value of 'IPAddresses' key or false for validation error
-                       [ null, [ '0.0.0.0/0', '::/0' ] ],
-                       [ '', [] ],
-                       [ "1.2.3.4\n::/0", [ '1.2.3.4', '::/0' ] ],
-                       [ "1.2.3.4\n::/x", false ],
-               ];
-       }
-}
diff --git a/tests/phpunit/includes/http/GuzzleHttpRequestTest.php b/tests/phpunit/includes/http/GuzzleHttpRequestTest.php
deleted file mode 100644 (file)
index c9356b6..0000000
+++ /dev/null
@@ -1,151 +0,0 @@
-<?php
-
-use GuzzleHttp\Handler\MockHandler;
-use GuzzleHttp\HandlerStack;
-use GuzzleHttp\Psr7\Response;
-use GuzzleHttp\Psr7\Request;
-
-/**
- * class for tests of GuzzleHttpRequest
- *
- * No actual requests are made herein - all external communications are mocked
- *
- * @covers GuzzleHttpRequest
- * @covers MWHttpRequest
- */
-class GuzzleHttpRequestTest extends MediaWikiTestCase {
-       /**
-        * Placeholder url to use for various tests.  This is never contacted, but we must use
-        * a url of valid format to avoid validation errors.
-        * @var string
-        */
-       protected $exampleUrl = 'http://www.example.test';
-
-       /**
-        * Minimal example body text
-        * @var string
-        */
-       protected $exampleBodyText = 'x';
-
-       /**
-        * For accumulating callback data for testing
-        * @var string
-        */
-       protected $bodyTextReceived = '';
-
-       /**
-        * Callback: process a chunk of the result of a HTTP request
-        *
-        * @param mixed $req
-        * @param string $buffer
-        * @return int Number of bytes handled
-        */
-       public function processHttpDataChunk( $req, $buffer ) {
-               $this->bodyTextReceived .= $buffer;
-               return strlen( $buffer );
-       }
-
-       public function testSuccess() {
-               $handler = HandlerStack::create( new MockHandler( [ new Response( 200, [
-                       'status' => 200,
-               ], $this->exampleBodyText ) ] ) );
-               $r = new GuzzleHttpRequest( $this->exampleUrl, [ 'handler' => $handler ] );
-               $r->execute();
-
-               $this->assertEquals( 200, $r->getStatus() );
-               $this->assertEquals( $this->exampleBodyText, $r->getContent() );
-       }
-
-       public function testSuccessConstructorCallback() {
-               $this->bodyTextReceived = '';
-               $handler = HandlerStack::create( new MockHandler( [ new Response( 200, [
-                       'status' => 200,
-               ], $this->exampleBodyText ) ] ) );
-               $r = new GuzzleHttpRequest( $this->exampleUrl, [
-                       'callback' => [ $this, 'processHttpDataChunk' ],
-                       'handler' => $handler,
-               ] );
-               $r->execute();
-
-               $this->assertEquals( 200, $r->getStatus() );
-               $this->assertEquals( $this->exampleBodyText, $this->bodyTextReceived );
-       }
-
-       public function testSuccessSetCallback() {
-               $this->bodyTextReceived = '';
-               $handler = HandlerStack::create( new MockHandler( [ new Response( 200, [
-                       'status' => 200,
-               ], $this->exampleBodyText ) ] ) );
-               $r = new GuzzleHttpRequest( $this->exampleUrl, [
-                       'handler' => $handler,
-               ] );
-               $r->setCallback( [ $this, 'processHttpDataChunk' ] );
-               $r->execute();
-
-               $this->assertEquals( 200, $r->getStatus() );
-               $this->assertEquals( $this->exampleBodyText, $this->bodyTextReceived );
-       }
-
-       /**
-        * use a callback stream to pipe the mocked response data to our callback function
-        */
-       public function testSuccessSink() {
-               $this->bodyTextReceived = '';
-               $handler = HandlerStack::create( new MockHandler( [ new Response( 200, [
-                       'status' => 200,
-               ], $this->exampleBodyText ) ] ) );
-               $r = new GuzzleHttpRequest( $this->exampleUrl, [
-                       'handler' => $handler,
-                       'sink' => new MWCallbackStream( [ $this, 'processHttpDataChunk' ] ),
-               ] );
-               $r->execute();
-
-               $this->assertEquals( 200, $r->getStatus() );
-               $this->assertEquals( $this->exampleBodyText, $this->bodyTextReceived );
-       }
-
-       public function testBadUrl() {
-               $r = new GuzzleHttpRequest( '' );
-               $s = $r->execute();
-               $errorMsg = $s->getErrorsByType( 'error' )[0]['message'];
-
-               $this->assertEquals( 0, $r->getStatus() );
-               $this->assertEquals( 'http-invalid-url', $errorMsg );
-       }
-
-       public function testConnectException() {
-               $handler = HandlerStack::create( new MockHandler( [ new GuzzleHttp\Exception\ConnectException(
-                       'Mock Connection Exception', new Request( 'GET', $this->exampleUrl )
-               ) ] ) );
-               $r = new GuzzleHttpRequest( $this->exampleUrl, [ 'handler' => $handler ] );
-               $s = $r->execute();
-               $errorMsg = $s->getErrorsByType( 'error' )[0]['message'];
-
-               $this->assertEquals( 0, $r->getStatus() );
-               $this->assertEquals( 'http-request-error', $errorMsg );
-       }
-
-       public function testTimeout() {
-               $handler = HandlerStack::create( new MockHandler( [ new GuzzleHttp\Exception\RequestException(
-                       'Connection timed out', new Request( 'GET', $this->exampleUrl )
-               ) ] ) );
-               $r = new GuzzleHttpRequest( $this->exampleUrl, [ 'handler' => $handler ] );
-               $s = $r->execute();
-               $errorMsg = $s->getErrorsByType( 'error' )[0]['message'];
-
-               $this->assertEquals( 0, $r->getStatus() );
-               $this->assertEquals( 'http-timed-out', $errorMsg );
-       }
-
-       public function testNotFound() {
-               $handler = HandlerStack::create( new MockHandler( [ new Response( 404, [
-                       'status' => '404',
-               ] ) ] ) );
-               $r = new GuzzleHttpRequest( $this->exampleUrl, [ 'handler' => $handler ] );
-               $s = $r->execute();
-               $errorMsg = $s->getErrorsByType( 'error' )[0]['message'];
-
-               $this->assertEquals( 404, $r->getStatus() );
-               $this->assertEquals( 'http-bad-status', $errorMsg );
-       }
-}
diff --git a/tests/phpunit/includes/http/HttpRequestFactoryTest.php b/tests/phpunit/includes/http/HttpRequestFactoryTest.php
deleted file mode 100644 (file)
index 7429dcc..0000000
+++ /dev/null
@@ -1,119 +0,0 @@
-<?php
-
-use MediaWiki\Http\HttpRequestFactory;
-
-/**
- * @covers MediaWiki\Http\HttpRequestFactory
- */
-class HttpRequestFactoryTest extends MediaWikiTestCase {
-
-       /**
-        * @return HttpRequestFactory
-        */
-       private function newFactory() {
-               return new HttpRequestFactory();
-       }
-
-       /**
-        * @return HttpRequestFactory
-        */
-       private function newFactoryWithFakeRequest(
-               MWHttpRequest $req,
-               $expectedUrl,
-               $expectedOptions = []
-       ) {
-               $factory = $this->getMockBuilder( HttpRequestFactory::class )
-                       ->setMethods( [ 'create' ] )
-                       ->getMock();
-
-               $factory->method( 'create' )
-                       ->willReturnCallback(
-                               function ( $url, array $options = [], $caller = __METHOD__ )
-                                       use ( $req, $expectedUrl, $expectedOptions )
-                               {
-                                       $this->assertSame( $url, $expectedUrl );
-
-                                       foreach ( $expectedOptions as $opt => $exp ) {
-                                               $this->assertArrayHasKey( $opt, $options );
-                                               $this->assertSame( $exp, $options[$opt] );
-                                       }
-
-                                       return $req;
-                               }
-                       );
-
-               return $factory;
-       }
-
-       /**
-        * @return MWHttpRequest
-        */
-       private function newFakeRequest( $result ) {
-               $req = $this->getMockBuilder( MWHttpRequest::class )
-                       ->disableOriginalConstructor()
-                       ->setMethods( [ 'getContent', 'execute' ] )
-                       ->getMock();
-
-               if ( $result instanceof Status ) {
-                       $req->method( 'getContent' )
-                               ->willReturn( $result->getValue() );
-                       $req->method( 'execute' )
-                               ->willReturn( $result );
-               } else {
-                       $req->method( 'getContent' )
-                               ->willReturn( $result );
-                       $req->method( 'execute' )
-                               ->willReturn( Status::newGood( $result ) );
-               }
-
-               return $req;
-       }
-
-       public function testCreate() {
-               $factory = $this->newFactory();
-               $this->assertInstanceOf( 'MWHttpRequest', $factory->create( 'http://example.test' ) );
-       }
-
-       public function testGetUserAgent() {
-               $factory = $this->newFactory();
-               $this->assertStringStartsWith( 'MediaWiki/', $factory->getUserAgent() );
-       }
-
-       public function testGet() {
-               $req = $this->newFakeRequest( __METHOD__ );
-               $factory = $this->newFactoryWithFakeRequest(
-                       $req, 'https://example.test', [ 'method' => 'GET' ]
-               );
-
-               $this->assertSame( __METHOD__, $factory->get( 'https://example.test' ) );
-       }
-
-       public function testPost() {
-               $req = $this->newFakeRequest( __METHOD__ );
-               $factory = $this->newFactoryWithFakeRequest(
-                       $req, 'https://example.test', [ 'method' => 'POST' ]
-               );
-
-               $this->assertSame( __METHOD__, $factory->post( 'https://example.test' ) );
-       }
-
-       public function testRequest() {
-               $req = $this->newFakeRequest( __METHOD__ );
-               $factory = $this->newFactoryWithFakeRequest(
-                       $req, 'https://example.test', [ 'method' => 'GET' ]
-               );
-
-               $this->assertSame( __METHOD__, $factory->request( 'GET', 'https://example.test' ) );
-       }
-
-       public function testRequest_failed() {
-               $status = Status::newFatal( 'testing' );
-               $req = $this->newFakeRequest( $status );
-               $factory = $this->newFactoryWithFakeRequest(
-                       $req, 'https://example.test', [ 'method' => 'POST' ]
-               );
-
-               $this->assertNull( $factory->request( 'POST', 'https://example.test' ) );
-       }
-
-}
diff --git a/tests/phpunit/includes/installer/InstallDocFormatterTest.php b/tests/phpunit/includes/installer/InstallDocFormatterTest.php
deleted file mode 100644 (file)
index 9584d4b..0000000
+++ /dev/null
@@ -1,83 +0,0 @@
-<?php
-
-class InstallDocFormatterTest extends MediaWikiTestCase {
-       /**
-        * @covers InstallDocFormatter
-        * @dataProvider provideDocFormattingTests
-        */
-       public function testFormat( $expected, $unformattedText, $message = '' ) {
-               $this->assertEquals(
-                       $expected,
-                       InstallDocFormatter::format( $unformattedText ),
-                       $message
-               );
-       }
-
-       /**
-        * Provider for testFormat()
-        */
-       public static function provideDocFormattingTests() {
-               # Format: (expected string, unformattedText string, optional message)
-               return [
-                       # Escape some wikitext
-                       [ 'Install &lt;tag>', 'Install <tag>', 'Escaping <' ],
-                       [ 'Install &#123;&#123;template}}', 'Install {{template}}', 'Escaping [[' ],
-                       [ 'Install &#91;&#91;page]]', 'Install [[page]]', 'Escaping {{' ],
-                       [ 'Install &#95;&#95;TOC&#95;&#95;', 'Install __TOC__', 'Escaping __' ],
-                       [ 'Install ', "Install \r", 'Removing \r' ],
-
-                       # Transform \t{1,2} into :{1,2}
-                       [ ':One indentation', "\tOne indentation", 'Replacing a single \t' ],
-                       [ '::Two indentations', "\t\tTwo indentations", 'Replacing 2 x \t' ],
-
-                       # Transform 'T123' links
-                       [
-                               '<span class="config-plainlink">[https://phabricator.wikimedia.org/T123 T123]</span>',
-                               'T123', 'Testing T123 links' ],
-                       [
-                               'bug <span class="config-plainlink">[https://phabricator.wikimedia.org/T123 T123]</span>',
-                               'bug T123', 'Testing bug T123 links' ],
-                       [
-                               '(<span class="config-plainlink">[https://phabricator.wikimedia.org/T987654 T987654]</span>)',
-                               '(T987654)', 'Testing (T987654) links' ],
-
-                       # "Tabc" shouldn't work
-                       [ 'Tfoobar', 'Tfoobar', "Don't match T followed by non-digits" ],
-                       [ 'T!!fakefake!!', 'T!!fakefake!!', "Don't match T followed by non-digits" ],
-
-                       # Transform 'bug 123' links
-                       [
-                               '<span class="config-plainlink">[https://bugzilla.wikimedia.org/123 bug 123]</span>',
-                               'bug 123', 'Testing bug 123 links' ],
-                       [
-                               '(<span class="config-plainlink">[https://bugzilla.wikimedia.org/987654 bug 987654]</span>)',
-                               '(bug 987654)', 'Testing (bug 987654) links' ],
-
-                       # "bug abc" shouldn't work
-                       [ 'bug foobar', 'bug foobar', "Don't match bug followed by non-digits" ],
-                       [ 'bug !!fakefake!!', 'bug !!fakefake!!', "Don't match bug followed by non-digits" ],
-
-                       # Transform '$wgFooBar' links
-                       [
-                               '<span class="config-plainlink">'
-                                       . '[https://www.mediawiki.org/wiki/Manual:$wgFooBar $wgFooBar]</span>',
-                               '$wgFooBar', 'Testing basic $wgFooBar' ],
-                       [
-                               '<span class="config-plainlink">'
-                                       . '[https://www.mediawiki.org/wiki/Manual:$wgFooBar45 $wgFooBar45]</span>',
-                               '$wgFooBar45', 'Testing $wgFooBar45 (with numbers)' ],
-                       [
-                               '<span class="config-plainlink">'
-                                       . '[https://www.mediawiki.org/wiki/Manual:$wgFoo_Bar $wgFoo_Bar]</span>',
-                               '$wgFoo_Bar', 'Testing $wgFoo_Bar (with underscore)' ],
-
-                       # Icky variables that shouldn't link
-                       [
-                               '$myAwesomeVariable',
-                               '$myAwesomeVariable',
-                               'Testing $myAwesomeVariable (not starting with $wg)'
-                       ],
-                       [ '$()not!a&Var', '$()not!a&Var', 'Testing $()not!a&Var (obviously not a variable)' ],
-               ];
-       }
-}
diff --git a/tests/phpunit/includes/installer/OracleInstallerTest.php b/tests/phpunit/includes/installer/OracleInstallerTest.php
deleted file mode 100644 (file)
index e255089..0000000
+++ /dev/null
@@ -1,49 +0,0 @@
-<?php
-
-/**
- * @group Database
- * @group Installer
- */
-class OracleInstallerTest extends MediaWikiTestCase {
-
-       /**
-        * @dataProvider provideOracleConnectStrings
-        * @covers OracleInstaller::checkConnectStringFormat
-        */
-       public function testCheckConnectStringFormat( $expected, $connectString, $msg = '' ) {
-               $validity = $expected ? 'should be valid' : 'should NOT be valid';
-               $msg = "'$connectString' ($msg) $validity.";
-               $this->assertEquals( $expected,
-                       OracleInstaller::checkConnectStringFormat( $connectString ),
-                       $msg
-               );
-       }
-
-       /**
-        * Provider to test OracleInstaller::checkConnectStringFormat()
-        */
-       function provideOracleConnectStrings() {
-               // expected result, connectString[, message]
-               return [
-                       [ true, 'simple_01', 'Simple TNS name' ],
-                       [ true, 'simple_01.world', 'TNS name with domain' ],
-                       [ true, 'simple_01.domain.net', 'TNS name with domain' ],
-                       [ true, 'host123', 'Host only' ],
-                       [ true, 'host123.domain.net', 'FQDN only' ],
-                       [ true, '//host123.domain.net', 'FQDN URL only' ],
-                       [ true, '123.223.213.132', 'Host IP only' ],
-                       [ true, 'host:1521', 'Host and port' ],
-                       [ true, 'host:1521/service', 'Host, port and service' ],
-                       [ true, 'host:1521/service:shared', 'Host, port, service and shared server type' ],
-                       [ true, 'host:1521/service:dedicated', 'Host, port, service and dedicated server type' ],
-                       [ true, 'host:1521/service:pooled', 'Host, port, service and pooled server type' ],
-                       [
-                               true,
-                               'host:1521/service:shared/instance1',
-                               'Host, port, service, server type and instance'
-                       ],
-                       [ true, 'host:1521//instance1', 'Host, port and instance' ],
-               ];
-       }
-
-}
diff --git a/tests/phpunit/includes/interwiki/InterwikiLookupAdapterTest.php b/tests/phpunit/includes/interwiki/InterwikiLookupAdapterTest.php
deleted file mode 100644 (file)
index 0a13de1..0000000
+++ /dev/null
@@ -1,133 +0,0 @@
-<?php
-
-use MediaWiki\Interwiki\InterwikiLookupAdapter;
-
-/**
- * @covers MediaWiki\Interwiki\InterwikiLookupAdapter
- *
- * @group MediaWiki
- * @group Interwiki
- */
-class InterwikiLookupAdapterTest extends MediaWikiTestCase {
-
-       /**
-        * @var InterwikiLookupAdapter
-        */
-       private $interwikiLookup;
-
-       protected function setUp() {
-               parent::setUp();
-
-               $this->interwikiLookup = new InterwikiLookupAdapter(
-                       $this->getSiteLookup( $this->getSites() )
-               );
-       }
-
-       public function testIsValidInterwiki() {
-               $this->assertTrue(
-                       $this->interwikiLookup->isValidInterwiki( 'enwt' ),
-                       'enwt known prefix is valid'
-               );
-               $this->assertTrue(
-                       $this->interwikiLookup->isValidInterwiki( 'foo' ),
-                       'foo site known prefix is valid'
-               );
-               $this->assertFalse(
-                       $this->interwikiLookup->isValidInterwiki( 'xyz' ),
-                       'unknown prefix is not valid'
-               );
-       }
-
-       public function testFetch() {
-               $interwiki = $this->interwikiLookup->fetch( '' );
-               $this->assertNull( $interwiki );
-
-               $interwiki = $this->interwikiLookup->fetch( 'xyz' );
-               $this->assertFalse( $interwiki );
-
-               $interwiki = $this->interwikiLookup->fetch( 'foo' );
-               $this->assertInstanceOf( Interwiki::class, $interwiki );
-               $this->assertSame( 'foobar', $interwiki->getWikiID() );
-
-               $interwiki = $this->interwikiLookup->fetch( 'enwt' );
-               $this->assertInstanceOf( Interwiki::class, $interwiki );
-
-               $this->assertSame( 'https://en.wiktionary.org/wiki/$1', $interwiki->getURL(), 'getURL' );
-               $this->assertSame( 'https://en.wiktionary.org/w/api.php', $interwiki->getAPI(), 'getAPI' );
-               $this->assertSame( 'enwiktionary', $interwiki->getWikiID(), 'getWikiID' );
-               $this->assertTrue( $interwiki->isLocal(), 'isLocal' );
-       }
-
-       public function testGetAllPrefixes() {
-               $foo = [
-                       'iw_prefix' => 'foo',
-                       'iw_url' => '',
-                       'iw_api' => '',
-                       'iw_wikiid' => 'foobar',
-                       'iw_local' => false,
-                       'iw_trans' => false,
-               ];
-               $enwt = [
-                       'iw_prefix' => 'enwt',
-                       'iw_url' => 'https://en.wiktionary.org/wiki/$1',
-                       'iw_api' => 'https://en.wiktionary.org/w/api.php',
-                       'iw_wikiid' => 'enwiktionary',
-                       'iw_local' => true,
-                       'iw_trans' => false,
-               ];
-
-               $this->assertEquals(
-                       [ $foo, $enwt ],
-                       $this->interwikiLookup->getAllPrefixes(),
-                       'getAllPrefixes()'
-               );
-
-               $this->assertEquals(
-                       [ $foo ],
-                       $this->interwikiLookup->getAllPrefixes( false ),
-                       'get external prefixes'
-               );
-
-               $this->assertEquals(
-                       [ $enwt ],
-                       $this->interwikiLookup->getAllPrefixes( true ),
-                       'get local prefixes'
-               );
-       }
-
-       private function getSiteLookup( SiteList $sites ) {
-               $siteLookup = $this->getMockBuilder( SiteLookup::class )
-                       ->disableOriginalConstructor()
-                       ->getMock();
-
-               $siteLookup->expects( $this->any() )
-                       ->method( 'getSites' )
-                       ->will( $this->returnValue( $sites ) );
-
-               return $siteLookup;
-       }
-
-       private function getSites() {
-               $sites = [];
-
-               $site = new Site();
-               $site->setGlobalId( 'foobar' );
-               $site->addInterwikiId( 'foo' );
-               $site->setSource( 'external' );
-               $sites[] = $site;
-
-               $site = new MediaWikiSite();
-               $site->setGlobalId( 'enwiktionary' );
-               $site->setGroup( 'wiktionary' );
-               $site->setLanguageCode( 'en' );
-               $site->addNavigationId( 'enwiktionary' );
-               $site->addInterwikiId( 'enwt' );
-               $site->setSource( 'local' );
-               $site->setPath( MediaWikiSite::PATH_PAGE, "https://en.wiktionary.org/wiki/$1" );
-               $site->setPath( MediaWikiSite::PATH_FILE, "https://en.wiktionary.org/w/$1" );
-               $sites[] = $site;
-
-               return new SiteList( $sites );
-       }
-
-}
diff --git a/tests/phpunit/includes/jobqueue/JobQueueMemoryTest.php b/tests/phpunit/includes/jobqueue/JobQueueMemoryTest.php
deleted file mode 100644 (file)
index 232b46a..0000000
+++ /dev/null
@@ -1,63 +0,0 @@
-<?php
-
-/**
- * @covers JobQueueMemory
- *
- * @group JobQueue
- *
- * @license GPL-2.0-or-later
- * @author Thiemo Kreuz
- */
-class JobQueueMemoryTest extends PHPUnit\Framework\TestCase {
-
-       use MediaWikiCoversValidator;
-
-       /**
-        * @return JobQueueMemory
-        */
-       private function newJobQueue() {
-               return JobQueue::factory( [
-                       'class' => JobQueueMemory::class,
-                       'domain' => WikiMap::getCurrentWikiDbDomain()->getId(),
-                       'type' => 'null',
-               ] );
-       }
-
-       private function newJobSpecification() {
-               return new JobSpecification(
-                       'null',
-                       [ 'customParameter' => null ],
-                       [],
-                       Title::newFromText( 'Custom title' )
-               );
-       }
-
-       public function testGetAllQueuedJobs() {
-               $queue = $this->newJobQueue();
-               $this->assertCount( 0, $queue->getAllQueuedJobs() );
-
-               $queue->push( $this->newJobSpecification() );
-               $this->assertCount( 1, $queue->getAllQueuedJobs() );
-       }
-
-       public function testGetAllAcquiredJobs() {
-               $queue = $this->newJobQueue();
-               $this->assertCount( 0, $queue->getAllAcquiredJobs() );
-
-               $queue->push( $this->newJobSpecification() );
-               $this->assertCount( 0, $queue->getAllAcquiredJobs() );
-
-               $queue->pop();
-               $this->assertCount( 1, $queue->getAllAcquiredJobs() );
-       }
-
-       public function testJobFromSpecInternal() {
-               $queue = $this->newJobQueue();
-               $job = $queue->jobFromSpecInternal( $this->newJobSpecification() );
-               $this->assertInstanceOf( Job::class, $job );
-               $this->assertSame( 'null', $job->getType() );
-               $this->assertArrayHasKey( 'customParameter', $job->getParams() );
-               $this->assertSame( 'Custom title', $job->getTitle()->getText() );
-       }
-
-}
diff --git a/tests/phpunit/includes/json/FormatJsonTest.php b/tests/phpunit/includes/json/FormatJsonTest.php
deleted file mode 100644 (file)
index a6adf34..0000000
+++ /dev/null
@@ -1,436 +0,0 @@
-<?php
-
-/**
- * @covers FormatJson
- */
-class FormatJsonTest extends MediaWikiTestCase {
-
-       public static function provideEncoderPrettyPrinting() {
-               return [
-                       // Four spaces
-                       [ true, '    ' ],
-                       [ '    ', '    ' ],
-                       // Two spaces
-                       [ '  ', '  ' ],
-                       // One tab
-                       [ "\t", "\t" ],
-               ];
-       }
-
-       /**
-        * @dataProvider provideEncoderPrettyPrinting
-        */
-       public function testEncoderPrettyPrinting( $pretty, $expectedIndent ) {
-               $obj = [
-                       'emptyObject' => new stdClass,
-                       'emptyArray' => [],
-                       'string' => 'foobar\\',
-                       'filledArray' => [
-                               [
-                                       123,
-                                       456,
-                               ],
-                               // Nested json works without problems
-                               '"7":["8",{"9":"10"}]',
-                               // Whitespace clean up doesn't touch strings that look alike
-                               "{\n\t\"emptyObject\": {\n\t},\n\t\"emptyArray\": [ ]\n}",
-                       ],
-               ];
-
-               // No trailing whitespace, no trailing linefeed
-               $json = '{
-       "emptyObject": {},
-       "emptyArray": [],
-       "string": "foobar\\\\",
-       "filledArray": [
-               [
-                       123,
-                       456
-               ],
-               "\"7\":[\"8\",{\"9\":\"10\"}]",
-               "{\n\t\"emptyObject\": {\n\t},\n\t\"emptyArray\": [ ]\n}"
-       ]
-}';
-
-               $json = str_replace( "\r", '', $json ); // Windows compat
-               $json = str_replace( "\t", $expectedIndent, $json );
-               $this->assertSame( $json, FormatJson::encode( $obj, $pretty ) );
-       }
-
-       public static function provideEncodeDefault() {
-               return self::getEncodeTestCases( [] );
-       }
-
-       /**
-        * @dataProvider provideEncodeDefault
-        */
-       public function testEncodeDefault( $from, $to ) {
-               $this->assertSame( $to, FormatJson::encode( $from ) );
-       }
-
-       public static function provideEncodeUtf8() {
-               return self::getEncodeTestCases( [ 'unicode' ] );
-       }
-
-       /**
-        * @dataProvider provideEncodeUtf8
-        */
-       public function testEncodeUtf8( $from, $to ) {
-               $this->assertSame( $to, FormatJson::encode( $from, false, FormatJson::UTF8_OK ) );
-       }
-
-       public static function provideEncodeXmlMeta() {
-               return self::getEncodeTestCases( [ 'xmlmeta' ] );
-       }
-
-       /**
-        * @dataProvider provideEncodeXmlMeta
-        */
-       public function testEncodeXmlMeta( $from, $to ) {
-               $this->assertSame( $to, FormatJson::encode( $from, false, FormatJson::XMLMETA_OK ) );
-       }
-
-       public static function provideEncodeAllOk() {
-               return self::getEncodeTestCases( [ 'unicode', 'xmlmeta' ] );
-       }
-
-       /**
-        * @dataProvider provideEncodeAllOk
-        */
-       public function testEncodeAllOk( $from, $to ) {
-               $this->assertSame( $to, FormatJson::encode( $from, false, FormatJson::ALL_OK ) );
-       }
-
-       public function testEncodePhpBug46944() {
-               $this->assertNotEquals(
-                       '\ud840\udc00',
-                       strtolower( FormatJson::encode( "\xf0\xa0\x80\x80" ) ),
-                       'Test encoding an broken json_encode character (U+20000)'
-               );
-       }
-
-       public function testEncodeFail() {
-               // Set up a recursive object that can't be encoded.
-               $a = new stdClass;
-               $b = new stdClass;
-               $a->b = $b;
-               $b->a = $a;
-               $this->assertFalse( FormatJson::encode( $a ) );
-       }
-
-       public function testDecodeReturnType() {
-               $this->assertInternalType(
-                       'object',
-                       FormatJson::decode( '{"Name": "Cheeso", "Rank": 7}' ),
-                       'Default to object'
-               );
-
-               $this->assertInternalType(
-                       'array',
-                       FormatJson::decode( '{"Name": "Cheeso", "Rank": 7}', true ),
-                       'Optional array'
-               );
-       }
-
-       public static function provideParse() {
-               return [
-                       [ null ],
-                       [ true ],
-                       [ false ],
-                       [ 0 ],
-                       [ 1 ],
-                       [ 1.2 ],
-                       [ '' ],
-                       [ 'str' ],
-                       [ [ 0, 1, 2 ] ],
-                       [ [ 'a' => 'b' ] ],
-                       [ [ 'a' => 'b' ] ],
-                       [ [ 'a' => 'b', 'x' => [ 'c' => 'd' ] ] ],
-               ];
-       }
-
-       /**
-        * Recursively convert arrays into stdClass
-        * @param array|string|bool|int|float|null $value
-        * @return stdClass|string|bool|int|float|null
-        */
-       public static function toObject( $value ) {
-               return !is_array( $value ) ? $value : (object)array_map( __METHOD__, $value );
-       }
-
-       /**
-        * @dataProvider provideParse
-        * @param mixed $value
-        */
-       public function testParse( $value ) {
-               $expected = self::toObject( $value );
-               $json = FormatJson::encode( $expected, false, FormatJson::ALL_OK );
-               $this->assertJson( $json );
-
-               $st = FormatJson::parse( $json );
-               $this->assertInstanceOf( Status::class, $st );
-               $this->assertTrue( $st->isGood() );
-               $this->assertEquals( $expected, $st->getValue() );
-
-               $st = FormatJson::parse( $json, FormatJson::FORCE_ASSOC );
-               $this->assertInstanceOf( Status::class, $st );
-               $this->assertTrue( $st->isGood() );
-               $this->assertEquals( $value, $st->getValue() );
-       }
-
-       /**
-        * Test data for testParseTryFixing.
-        *
-        * Some PHP interpreters use json-c rather than the JSON.org canonical
-        * parser to avoid being encumbered by the "shall be used for Good, not
-        * Evil" clause of the JSON.org parser's license. By default, json-c
-        * parses in a non-strict mode which allows trailing commas for array and
-        * object delarations among other things, so our JSON_ERROR_SYNTAX rescue
-        * block is not always triggered. It however isn't lenient in exactly the
-        * same ways as our TRY_FIXING mode, so the assertions in this test are
-        * a bit more complicated than they ideally would be:
-        *
-        * Optional third argument: true if json-c parses the value without
-        * intervention, false otherwise. Defaults to true.
-        *
-        * Optional fourth argument: expected cannonical JSON serialization of
-        * json-c parsed result. Defaults to the second argument's value.
-        */
-       public static function provideParseTryFixing() {
-               return [
-                       [ "[,]", '[]', false ],
-                       [ "[ , ]", '[]', false ],
-                       [ "[ , }", false ],
-                       [ '[1],', false, true, '[1]' ],
-                       [ "[1,]", '[1]' ],
-                       [ "[1\n,]", '[1]' ],
-                       [ "[1,\n]", '[1]' ],
-                       [ "[1,]\n", '[1]' ],
-                       [ "[1\n,\n]\n", '[1]' ],
-                       [ '["a,",]', '["a,"]' ],
-                       [ "[[1,]\n,[2,\n],[3\n,]]", '[[1],[2],[3]]' ],
-                       // I wish we could parse this, but would need quote parsing
-                       [ '[[1,],[2,],[3,]]', false, true, '[[1],[2],[3]]' ],
-                       [ '[1,,]', false, false, '[1]' ],
-               ];
-       }
-
-       /**
-        * @dataProvider provideParseTryFixing
-        * @param string $value
-        * @param string|bool $expected Expected result with strict parser
-        * @param bool $jsoncParses Will json-c parse this value without TRY_FIXING?
-        * @param string|bool $expectedJsonc Expected result with lenient parser
-        * if different from the strict expectation
-        */
-       public function testParseTryFixing(
-               $value, $expected,
-               $jsoncParses = true, $expectedJsonc = null
-       ) {
-               // PHP5 results are always expected to have isGood() === false
-               $expectedGoodStatus = false;
-
-               // Check to see if json parser allows trailing commas
-               if ( json_decode( '[1,]' ) !== null ) {
-                       // Use json-c specific expected result if provided
-                       $expected = ( $expectedJsonc === null ) ? $expected : $expectedJsonc;
-                       // If json-c parses the value natively, expect isGood() === true
-                       $expectedGoodStatus = $jsoncParses;
-               }
-
-               $st = FormatJson::parse( $value, FormatJson::TRY_FIXING );
-               $this->assertInstanceOf( Status::class, $st );
-               if ( $expected === false ) {
-                       $this->assertFalse( $st->isOK(), 'Expected isOK() == false' );
-               } else {
-                       $this->assertSame( $expectedGoodStatus, $st->isGood(),
-                               'Expected isGood() == ' . ( $expectedGoodStatus ? 'true' : 'false' )
-                       );
-                       $this->assertTrue( $st->isOK(), 'Expected isOK == true' );
-                       $val = FormatJson::encode( $st->getValue(), false, FormatJson::ALL_OK );
-                       $this->assertEquals( $expected, $val );
-               }
-       }
-
-       public static function provideParseErrors() {
-               return [
-                       [ 'aaa' ],
-                       [ '{"j": 1 ] }' ],
-               ];
-       }
-
-       /**
-        * @dataProvider provideParseErrors
-        * @param mixed $value
-        */
-       public function testParseErrors( $value ) {
-               $st = FormatJson::parse( $value );
-               $this->assertInstanceOf( Status::class, $st );
-               $this->assertFalse( $st->isOK() );
-       }
-
-       public function provideStripComments() {
-               return [
-                       [ '{"a":"b"}', '{"a":"b"}' ],
-                       [ "{\"a\":\"b\"}\n", "{\"a\":\"b\"}\n" ],
-                       [ '/*c*/{"c":"b"}', '{"c":"b"}' ],
-                       [ '{"a":"c"}/*c*/', '{"a":"c"}' ],
-                       [ '/*c//d*/{"c":"b"}', '{"c":"b"}' ],
-                       [ '{/*c*/"c":"b"}', '{"c":"b"}' ],
-                       [ "/*\nc\r\n*/{\"c\":\"b\"}", '{"c":"b"}' ],
-                       [ "//c\n{\"c\":\"b\"}", '{"c":"b"}' ],
-                       [ "//c\r\n{\"c\":\"b\"}", '{"c":"b"}' ],
-                       [ '{"a":"c"}//c', '{"a":"c"}' ],
-                       [ "{\"a-c\"://c\n\"b\"}", '{"a-c":"b"}' ],
-                       [ '{"/*a":"b"}', '{"/*a":"b"}' ],
-                       [ '{"a":"//b"}', '{"a":"//b"}' ],
-                       [ '{"a":"b/*c*/"}', '{"a":"b/*c*/"}' ],
-                       [ "{\"\\\"/*a\":\"b\"}", "{\"\\\"/*a\":\"b\"}" ],
-                       [ '', '' ],
-                       [ '/*c', '' ],
-                       [ '//c', '' ],
-                       [ '"http://example.com"', '"http://example.com"' ],
-                       [ "\0", "\0" ],
-                       [ '"Blåbærsyltetøy"', '"Blåbærsyltetøy"' ],
-               ];
-       }
-
-       /**
-        * @covers FormatJson::stripComments
-        * @dataProvider provideStripComments
-        * @param string $json
-        * @param string $expect
-        */
-       public function testStripComments( $json, $expect ) {
-               $this->assertSame( $expect, FormatJson::stripComments( $json ) );
-       }
-
-       public function provideParseStripComments() {
-               return [
-                       [ '/* blah */true', true ],
-                       [ "// blah \ntrue", true ],
-                       [ '[ "a" , /* blah */ "b" ]', [ 'a', 'b' ] ],
-               ];
-       }
-
-       /**
-        * @covers FormatJson::parse
-        * @covers FormatJson::stripComments
-        * @dataProvider provideParseStripComments
-        * @param string $json
-        * @param mixed $expect
-        */
-       public function testParseStripComments( $json, $expect ) {
-               $st = FormatJson::parse( $json, FormatJson::STRIP_COMMENTS );
-               $this->assertInstanceOf( Status::class, $st );
-               $this->assertTrue( $st->isGood() );
-               $this->assertEquals( $expect, $st->getValue() );
-       }
-
-       /**
-        * Generate a set of test cases for a particular combination of encoder options.
-        *
-        * @param array $unescapedGroups List of character groups to leave unescaped
-        * @return array Arrays of unencoded strings and corresponding encoded strings
-        */
-       private static function getEncodeTestCases( array $unescapedGroups ) {
-               $groups = [
-                       'always' => [
-                               // Forward slash (always unescaped)
-                               '/' => '/',
-
-                               // Control characters
-                               "\0" => '\u0000',
-                               "\x08" => '\b',
-                               "\t" => '\t',
-                               "\n" => '\n',
-                               "\r" => '\r',
-                               "\f" => '\f',
-                               "\x1f" => '\u001f', // representative example
-
-                               // Double quotes
-                               '"' => '\"',
-
-                               // Backslashes
-                               '\\' => '\\\\',
-                               '\\\\' => '\\\\\\\\',
-                               '\\u00e9' => '\\\u00e9', // security check for Unicode unescaping
-
-                               // Line terminators
-                               "\xe2\x80\xa8" => '\u2028',
-                               "\xe2\x80\xa9" => '\u2029',
-                       ],
-                       'unicode' => [
-                               "\xc3\xa9" => '\u00e9',
-                               "\xf0\x9d\x92\x9e" => '\ud835\udc9e', // U+1D49E, outside the BMP
-                       ],
-                       'xmlmeta' => [
-                               '<' => '\u003C', // JSON_HEX_TAG uses uppercase hex digits
-                               '>' => '\u003E',
-                               '&' => '\u0026',
-                       ],
-               ];
-
-               $cases = [];
-               foreach ( $groups as $name => $rules ) {
-                       $leaveUnescaped = in_array( $name, $unescapedGroups );
-                       foreach ( $rules as $from => $to ) {
-                               $cases[] = [ $from, '"' . ( $leaveUnescaped ? $from : $to ) . '"' ];
-                       }
-               }
-
-               return $cases;
-       }
-
-       public function provideEmptyJsonKeyStrings() {
-               return [
-                       [
-                               '{"":"foo"}',
-                               '{"":"foo"}',
-                               ''
-                       ],
-                       [
-                               '{"_empty_":"foo"}',
-                               '{"_empty_":"foo"}',
-                               '_empty_' ],
-                       [
-                               '{"\u005F\u0065\u006D\u0070\u0074\u0079\u005F":"foo"}',
-                               '{"_empty_":"foo"}',
-                               '_empty_'
-                       ],
-                       [
-                               '{"_empty_":"bar","":"foo"}',
-                               '{"_empty_":"bar","":"foo"}',
-                               ''
-                       ],
-                       [
-                               '{"":"bar","_empty_":"foo"}',
-                               '{"":"bar","_empty_":"foo"}',
-                               '_empty_'
-                       ]
-               ];
-       }
-
-       /**
-        * @covers FormatJson::encode
-        * @covers FormatJson::decode
-        * @dataProvider provideEmptyJsonKeyStrings
-        * @param string $json
-        *
-        * Decoding behavior with empty keys can be surprising.
-        * See https://phabricator.wikimedia.org/T206411
-        */
-       public function testEmptyJsonKeyArray( $json, $expect, $php71Name ) {
-               // Decoding to array is consistent across supported PHP versions
-               $this->assertSame( $expect, FormatJson::encode(
-                       FormatJson::decode( $json, true ) ) );
-
-               // Decoding to object differs between supported PHP versions
-               $obj = FormatJson::decode( $json );
-               if ( version_compare( PHP_VERSION, '7.1', '<' ) ) {
-                       $this->assertEquals( 'foo', $obj->_empty_ );
-               } else {
-                       $this->assertEquals( 'foo', $obj->{$php71Name} );
-               }
-       }
-}
diff --git a/tests/phpunit/includes/libs/ArrayUtilsTest.php b/tests/phpunit/includes/libs/ArrayUtilsTest.php
deleted file mode 100644 (file)
index 12b6320..0000000
+++ /dev/null
@@ -1,308 +0,0 @@
-<?php
-/**
- * Test class for ArrayUtils class
- *
- * @group Database
- */
-class ArrayUtilsTest extends PHPUnit\Framework\TestCase {
-
-       use MediaWikiCoversValidator;
-
-       /**
-        * @covers ArrayUtils::findLowerBound
-        * @dataProvider provideFindLowerBound
-        */
-       function testFindLowerBound(
-               $valueCallback, $valueCount, $comparisonCallback, $target, $expected
-       ) {
-               $this->assertSame(
-                       ArrayUtils::findLowerBound(
-                               $valueCallback, $valueCount, $comparisonCallback, $target
-                       ), $expected
-               );
-       }
-
-       function provideFindLowerBound() {
-               $indexValueCallback = function ( $size ) {
-                       return function ( $val ) use ( $size ) {
-                               $this->assertTrue( $val >= 0 );
-                               $this->assertTrue( $val < $size );
-                               return $val;
-                       };
-               };
-               $comparisonCallback = function ( $a, $b ) {
-                       return $a - $b;
-               };
-
-               return [
-                       [
-                               $indexValueCallback( 0 ),
-                               0,
-                               $comparisonCallback,
-                               1,
-                               false,
-                       ],
-                       [
-                               $indexValueCallback( 1 ),
-                               1,
-                               $comparisonCallback,
-                               -1,
-                               false,
-                       ],
-                       [
-                               $indexValueCallback( 1 ),
-                               1,
-                               $comparisonCallback,
-                               0,
-                               0,
-                       ],
-                       [
-                               $indexValueCallback( 1 ),
-                               1,
-                               $comparisonCallback,
-                               1,
-                               0,
-                       ],
-                       [
-                               $indexValueCallback( 2 ),
-                               2,
-                               $comparisonCallback,
-                               -1,
-                               false,
-                       ],
-                       [
-                               $indexValueCallback( 2 ),
-                               2,
-                               $comparisonCallback,
-                               0,
-                               0,
-                       ],
-                       [
-                               $indexValueCallback( 2 ),
-                               2,
-                               $comparisonCallback,
-                               0.5,
-                               0,
-                       ],
-                       [
-                               $indexValueCallback( 2 ),
-                               2,
-                               $comparisonCallback,
-                               1,
-                               1,
-                       ],
-                       [
-                               $indexValueCallback( 2 ),
-                               2,
-                               $comparisonCallback,
-                               1.5,
-                               1,
-                       ],
-                       [
-                               $indexValueCallback( 3 ),
-                               3,
-                               $comparisonCallback,
-                               1,
-                               1,
-                       ],
-                       [
-                               $indexValueCallback( 3 ),
-                               3,
-                               $comparisonCallback,
-                               1.5,
-                               1,
-                       ],
-                       [
-                               $indexValueCallback( 3 ),
-                               3,
-                               $comparisonCallback,
-                               2,
-                               2,
-                       ],
-                       [
-                               $indexValueCallback( 3 ),
-                               3,
-                               $comparisonCallback,
-                               3,
-                               2,
-                       ],
-               ];
-       }
-
-       /**
-        * @covers ArrayUtils::arrayDiffAssocRecursive
-        * @dataProvider provideArrayDiffAssocRecursive
-        */
-       function testArrayDiffAssocRecursive( $expected, ...$args ) {
-               $this->assertEquals( call_user_func_array(
-                       'ArrayUtils::arrayDiffAssocRecursive', $args
-               ), $expected );
-       }
-
-       function provideArrayDiffAssocRecursive() {
-               return [
-                       [
-                               [],
-                               [],
-                               [],
-                       ],
-                       [
-                               [],
-                               [],
-                               [],
-                               [],
-                       ],
-                       [
-                               [ 1 ],
-                               [ 1 ],
-                               [],
-                       ],
-                       [
-                               [ 1 ],
-                               [ 1 ],
-                               [],
-                               [],
-                       ],
-                       [
-                               [],
-                               [],
-                               [ 1 ],
-                       ],
-                       [
-                               [],
-                               [],
-                               [ 1 ],
-                               [ 2 ],
-                       ],
-                       [
-                               [ '' => 1 ],
-                               [ '' => 1 ],
-                               [],
-                       ],
-                       [
-                               [],
-                               [],
-                               [ '' => 1 ],
-                       ],
-                       [
-                               [ 1 ],
-                               [ 1 ],
-                               [ 2 ],
-                       ],
-                       [
-                               [],
-                               [ 1 ],
-                               [ 2 ],
-                               [ 1 ],
-                       ],
-                       [
-                               [],
-                               [ 1 ],
-                               [ 1, 2 ],
-                       ],
-                       [
-                               [ 1 => 1 ],
-                               [ 1 => 1 ],
-                               [ 1 ],
-                       ],
-                       [
-                               [],
-                               [ 1 => 1 ],
-                               [ 1 ],
-                               [ 1 => 1 ],
-                       ],
-                       [
-                               [],
-                               [ 1 => 1 ],
-                               [ 1, 1, 1 ],
-                       ],
-                       [
-                               [],
-                               [ [] ],
-                               [],
-                       ],
-                       [
-                               [],
-                               [ [ [] ] ],
-                               [],
-                       ],
-                       [
-                               [ 1, [ 1 ] ],
-                               [ 1, [ 1 ] ],
-                               [],
-                       ],
-                       [
-                               [ 1 ],
-                               [ 1, [ 1 ] ],
-                               [ 2, [ 1 ] ],
-                       ],
-                       [
-                               [],
-                               [ 1, [ 1 ] ],
-                               [ 2, [ 1 ] ],
-                               [ 1, [ 2 ] ],
-                       ],
-                       [
-                               [ 1 ],
-                               [ 1, [] ],
-                               [ 2 ],
-                       ],
-                       [
-                               [],
-                               [ 1, [] ],
-                               [ 2 ],
-                               [ 1 ],
-                       ],
-                       [
-                               [ 1, [ 1 => 2 ] ],
-                               [ 1, [ 1, 2 ] ],
-                               [ 2, [ 1 ] ],
-                       ],
-                       [
-                               [ 1 ],
-                               [ 1, [ 1, 2 ] ],
-                               [ 2, [ 1 ] ],
-                               [ 2, [ 1 => 2 ] ],
-                       ],
-                       [
-                               [ 1 => [ 1, 2 ] ],
-                               [ 1, [ 1, 2 ] ],
-                               [ 1, [ 2 ] ],
-                       ],
-                       [
-                               [ 1 => [ [ 2, 3 ], 2 ] ],
-                               [ 1, [ [ 2, 3 ], 2 ] ],
-                               [ 1, [ 2 ] ],
-                       ],
-                       [
-                               [ 1 => [ [ 2 ], 2 ] ],
-                               [ 1, [ [ 2, 3 ], 2 ] ],
-                               [ 1, [ [ 1 => 3 ] ] ],
-                       ],
-                       [
-                               [ 1 => [ 1 => 2 ] ],
-                               [ 1, [ [ 2, 3 ], 2 ] ],
-                               [ 1, [ [ 1 => 3, 0 => 2 ] ] ],
-                       ],
-                       [
-                               [ 1 => [ 1 => 2 ] ],
-                               [ 1, [ [ 2, 3 ], 2 ] ],
-                               [ 1, [ [ 1 => 3 ] ] ],
-                               [ 1 => [ [ 2 ] ] ],
-                       ],
-                       [
-                               [],
-                               [ 1, [ [ 2, 3 ], 2 ] ],
-                               [ 1 => [ 1 => 2, 0 => [ 1 => 3, 0 => 2 ] ], 0 => 1 ],
-                       ],
-                       [
-                               [],
-                               [ 1, [ [ 2, 3 ], 2 ] ],
-                               [ 1 => [ 1 => 2 ] ],
-                               [ 1 => [ [ 1 => 3 ] ] ],
-                               [ 1 => [ [ 2 ] ] ],
-                               [ 1 ],
-                       ],
-               ];
-       }
-}
diff --git a/tests/phpunit/includes/libs/CookieTest.php b/tests/phpunit/includes/libs/CookieTest.php
deleted file mode 100644 (file)
index e383be9..0000000
+++ /dev/null
@@ -1,52 +0,0 @@
-<?php
-
-/**
- * @covers Cookie
- */
-class CookieTest extends \PHPUnit\Framework\TestCase {
-
-       /**
-        * @dataProvider cookieDomains
-        * @covers Cookie::validateCookieDomain
-        */
-       public function testValidateCookieDomain( $expected, $domain, $origin = null ) {
-               if ( $origin ) {
-                       $ok = Cookie::validateCookieDomain( $domain, $origin );
-                       $msg = "$domain against origin $origin";
-               } else {
-                       $ok = Cookie::validateCookieDomain( $domain );
-                       $msg = "$domain";
-               }
-               $this->assertEquals( $expected, $ok, $msg );
-       }
-
-       public static function cookieDomains() {
-               return [
-                       [ false, "org" ],
-                       [ false, ".org" ],
-                       [ true, "wikipedia.org" ],
-                       [ true, ".wikipedia.org" ],
-                       [ false, "co.uk" ],
-                       [ false, ".co.uk" ],
-                       [ false, "gov.uk" ],
-                       [ false, ".gov.uk" ],
-                       [ true, "supermarket.uk" ],
-                       [ false, "uk" ],
-                       [ false, ".uk" ],
-                       [ false, "127.0.0." ],
-                       [ false, "127." ],
-                       [ false, "127.0.0.1." ],
-                       [ true, "127.0.0.1" ],
-                       [ false, "333.0.0.1" ],
-                       [ true, "example.com" ],
-                       [ false, "example.com." ],
-                       [ true, ".example.com" ],
-
-                       [ true, ".example.com", "www.example.com" ],
-                       [ false, "example.com", "www.example.com" ],
-                       [ true, "127.0.0.1", "127.0.0.1" ],
-                       [ false, "127.0.0.1", "localhost" ],
-               ];
-       }
-
-}
diff --git a/tests/phpunit/includes/libs/DeferredStringifierTest.php b/tests/phpunit/includes/libs/DeferredStringifierTest.php
deleted file mode 100644 (file)
index c9cdf58..0000000
+++ /dev/null
@@ -1,54 +0,0 @@
-<?php
-
-/**
- * @covers DeferredStringifier
- */
-class DeferredStringifierTest extends PHPUnit\Framework\TestCase {
-
-       use MediaWikiCoversValidator;
-
-       /**
-        * @dataProvider provideToString
-        */
-       public function testToString( $params, $expected ) {
-               $class = new ReflectionClass( DeferredStringifier::class );
-               $ds = $class->newInstanceArgs( $params );
-               $this->assertEquals( $expected, (string)$ds );
-       }
-
-       public static function provideToString() {
-               return [
-                       // No args
-                       [
-                               [
-                                       function () {
-                                               return 'foo';
-                                       }
-                               ],
-                               'foo'
-                       ],
-                       // Has args
-                       [
-                               [
-                                       function ( $i ) {
-                                               return $i;
-                                       },
-                                       'bar'
-                               ],
-                               'bar'
-                       ],
-               ];
-       }
-
-       /**
-        * Verify that the callback is not called if
-        * it is never converted to a string
-        */
-       public function testCallbackNotCalled() {
-               $ds = new DeferredStringifier( function () {
-                       throw new Exception( 'This should not be reached!' );
-               } );
-               // No exception was thrown
-               $this->assertTrue( true );
-       }
-}
diff --git a/tests/phpunit/includes/libs/DnsSrvDiscovererTest.php b/tests/phpunit/includes/libs/DnsSrvDiscovererTest.php
deleted file mode 100644 (file)
index 1b3397c..0000000
+++ /dev/null
@@ -1,144 +0,0 @@
-<?php
-
-/**
- * @covers DnsSrvDiscoverer
- */
-class DnsSrvDiscovererTest extends PHPUnit\Framework\TestCase {
-
-       use MediaWikiCoversValidator;
-
-       /**
-        * @dataProvider provideRecords
-        */
-       public function testPickServer( $params, $expected ) {
-               $discoverer = new DnsSrvDiscoverer( 'etcd-tcp.example.net' );
-               $record = $discoverer->pickServer( $params );
-
-               $this->assertEquals( $expected, $record );
-       }
-
-       public static function provideRecords() {
-               return [
-                       [
-                               [ // record list
-                                       [
-                                               'target' => 'conf03.example.net',
-                                               'port' => 'SRV',
-                                               'pri' => 0,
-                                               'weight' => 1,
-                                       ],
-                                       [
-                                               'target' => 'conf02.example.net',
-                                               'port' => 'SRV',
-                                               'pri' => 1,
-                                               'weight' => 1,
-                                       ],
-                                       [
-                                               'target' => 'conf01.example.net',
-                                               'port' => 'SRV',
-                                               'pri' => 2,
-                                               'weight' => 1,
-                                       ],
-                               ], // selected record
-                               [
-                                       'target' => 'conf03.example.net',
-                                       'port' => 'SRV',
-                                       'pri' => 0,
-                                       'weight' => 1,
-                               ]
-                       ],
-                       [
-                               [ // record list
-                                       [
-                                               'target' => 'conf03or2.example.net',
-                                               'port' => 'SRV',
-                                               'pri' => 0,
-                                               'weight' => 1,
-                                       ],
-                                       [
-                                               'target' => 'conf03or2.example.net',
-                                               'port' => 'SRV',
-                                               'pri' => 0,
-                                               'weight' => 1,
-                                       ],
-                                       [
-                                               'target' => 'conf01.example.net',
-                                               'port' => 'SRV',
-                                               'pri' => 2,
-                                               'weight' => 1,
-                                       ],
-                                       [
-                                               'target' => 'conf04.example.net',
-                                               'port' => 'SRV',
-                                               'pri' => 2,
-                                               'weight' => 1,
-                                       ],
-                                       [
-                                               'target' => 'conf05.example.net',
-                                               'port' => 'SRV',
-                                               'pri' => 3,
-                                               'weight' => 1,
-                                       ],
-                               ], // selected record
-                               [
-                                       'target' => 'conf03or2.example.net',
-                                       'port' => 'SRV',
-                                       'pri' => 0,
-                                       'weight' => 1,
-                               ]
-                       ],
-               ];
-       }
-
-       public function testRemoveServer() {
-               $dsd = new DnsSrvDiscoverer( 'localhost' );
-
-               $servers = [
-                       [
-                               'target' => 'conf01.example.net',
-                               'port' => 35,
-                               'pri' => 2,
-                               'weight' => 1,
-                       ],
-                       [
-                               'target' => 'conf04.example.net',
-                               'port' => 74,
-                               'pri' => 2,
-                               'weight' => 1,
-                       ],
-                       [
-                               'target' => 'conf05.example.net',
-                               'port' => 77,
-                               'pri' => 3,
-                               'weight' => 1,
-                       ],
-               ];
-               $server = $servers[1];
-
-               $expected = [
-                       [
-                               'target' => 'conf01.example.net',
-                               'port' => 35,
-                               'pri' => 2,
-                               'weight' => 1,
-                       ],
-                       [
-                               'target' => 'conf05.example.net',
-                               'port' => 77,
-                               'pri' => 3,
-                               'weight' => 1,
-                       ],
-               ];
-
-               $this->assertEquals(
-                       $expected,
-                       $dsd->removeServer( $server, $servers ),
-                       "Correct server removed"
-               );
-               $this->assertEquals(
-                       $expected,
-                       $dsd->removeServer( $server, $servers ),
-                       "Nothing to remove"
-               );
-       }
-}
diff --git a/tests/phpunit/includes/libs/EasyDeflateTest.php b/tests/phpunit/includes/libs/EasyDeflateTest.php
deleted file mode 100644 (file)
index da39d48..0000000
+++ /dev/null
@@ -1,64 +0,0 @@
-<?php
-/**
- * Copyright (C) 2018 Kunal Mehta <legoktm@member.fsf.org>
- *
- * 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; either version 2 of the License, or
- * (at your option) any later version.
- *
- * 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.
- *
- */
-
-/**
- * @covers EasyDeflate
- */
-class EasyDeflateTest extends PHPUnit\Framework\TestCase {
-
-       public function provideIsDeflated() {
-               return [
-                       [ 'rawdeflate,S8vPT0osAgA=', true ],
-                       [ 'abcdefghijklmnopqrstuvwxyz', false ],
-               ];
-       }
-
-       /**
-        * @dataProvider provideIsDeflated
-        */
-       public function testIsDeflated( $data, $expected ) {
-               $actual = EasyDeflate::isDeflated( $data );
-               $this->assertSame( $expected, $actual );
-       }
-
-       public function provideInflate() {
-               return [
-                       [ 'rawdeflate,S8vPT0osAgA=', true, 'foobar' ],
-                       // Fails base64_decode
-                       [ 'rawdeflate,🌻', false, 'easydeflate-invaliddeflate' ],
-                       // Fails gzinflate
-                       [ 'rawdeflate,S8vPT0dfdAgB=', false, 'easydeflate-invaliddeflate' ],
-               ];
-       }
-
-       /**
-        * @dataProvider provideInflate
-        */
-       public function testInflate( $data, $ok, $value ) {
-               $actual = EasyDeflate::inflate( $data );
-               if ( $ok ) {
-                       $this->assertTrue( $actual->isOK() );
-                       $this->assertSame( $value, $actual->getValue() );
-               } else {
-                       $this->assertFalse( $actual->isOK() );
-                       $this->assertTrue( $actual->hasMessage( $value ) );
-               }
-       }
-}
diff --git a/tests/phpunit/includes/libs/GenericArrayObjectTest.php b/tests/phpunit/includes/libs/GenericArrayObjectTest.php
deleted file mode 100644 (file)
index 3be2b06..0000000
+++ /dev/null
@@ -1,279 +0,0 @@
-<?php
-
-/**
- * Tests for the GenericArrayObject and deriving classes.
- *
- * 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; either version 2 of the License, or
- * (at your option) any later version.
- *
- * 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.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- * @since 1.20
- *
- * @ingroup Test
- * @group GenericArrayObject
- *
- * @author Jeroen De Dauw < jeroendedauw@gmail.com >
- */
-abstract class GenericArrayObjectTest extends PHPUnit\Framework\TestCase {
-
-       use MediaWikiCoversValidator;
-
-       /**
-        * Returns objects that can serve as elements in the concrete
-        * GenericArrayObject deriving class being tested.
-        *
-        * @since 1.20
-        *
-        * @return array
-        */
-       abstract public function elementInstancesProvider();
-
-       /**
-        * Returns the name of the concrete class being tested.
-        *
-        * @since 1.20
-        *
-        * @return string
-        */
-       abstract public function getInstanceClass();
-
-       /**
-        * Provides instances of the concrete class being tested.
-        *
-        * @since 1.20
-        *
-        * @return array
-        */
-       public function instanceProvider() {
-               $instances = [];
-
-               foreach ( $this->elementInstancesProvider() as $elementInstances ) {
-                       $instances[] = $this->getNew( $elementInstances[0] );
-               }
-
-               return $this->arrayWrap( $instances );
-       }
-
-       /**
-        * @since 1.20
-        *
-        * @param array $elements
-        *
-        * @return GenericArrayObject
-        */
-       protected function getNew( array $elements = [] ) {
-               $class = $this->getInstanceClass();
-
-               return new $class( $elements );
-       }
-
-       /**
-        * @dataProvider elementInstancesProvider
-        *
-        * @since 1.20
-        *
-        * @param array $elements
-        *
-        * @covers GenericArrayObject::__construct
-        */
-       public function testConstructor( array $elements ) {
-               $arrayObject = $this->getNew( $elements );
-
-               $this->assertEquals( count( $elements ), $arrayObject->count() );
-       }
-
-       /**
-        * @dataProvider elementInstancesProvider
-        *
-        * @since 1.20
-        *
-        * @param array $elements
-        *
-        * @covers GenericArrayObject::isEmpty
-        */
-       public function testIsEmpty( array $elements ) {
-               $arrayObject = $this->getNew( $elements );
-
-               $this->assertEquals( $elements === [], $arrayObject->isEmpty() );
-       }
-
-       /**
-        * @dataProvider instanceProvider
-        *
-        * @since 1.20
-        *
-        * @param GenericArrayObject $list
-        *
-        * @covers GenericArrayObject::offsetUnset
-        */
-       public function testUnset( GenericArrayObject $list ) {
-               if ( $list->isEmpty() ) {
-                       $this->assertTrue( true ); // We cannot test unset if there are no elements
-               } else {
-                       $offset = $list->getIterator()->key();
-                       $count = $list->count();
-                       $list->offsetUnset( $offset );
-                       $this->assertEquals( $count - 1, $list->count() );
-               }
-
-               if ( !$list->isEmpty() ) {
-                       $offset = $list->getIterator()->key();
-                       $count = $list->count();
-                       unset( $list[$offset] );
-                       $this->assertEquals( $count - 1, $list->count() );
-               }
-       }
-
-       /**
-        * @dataProvider elementInstancesProvider
-        *
-        * @since 1.20
-        *
-        * @param array $elements
-        *
-        * @covers GenericArrayObject::append
-        */
-       public function testAppend( array $elements ) {
-               $list = $this->getNew();
-
-               $listSize = count( $elements );
-
-               foreach ( $elements as $element ) {
-                       $list->append( $element );
-               }
-
-               $this->assertEquals( $listSize, $list->count() );
-
-               $list = $this->getNew();
-
-               foreach ( $elements as $element ) {
-                       $list[] = $element;
-               }
-
-               $this->assertEquals( $listSize, $list->count() );
-
-               $this->checkTypeChecks( function ( GenericArrayObject $list, $element ) {
-                       $list->append( $element );
-               } );
-       }
-
-       /**
-        * @since 1.20
-        *
-        * @param callable $function
-        */
-       protected function checkTypeChecks( $function ) {
-               $excption = null;
-               $list = $this->getNew();
-
-               $elementClass = $list->getObjectType();
-
-               foreach ( [ 42, 'foo', [], new stdClass(), 4.2 ] as $element ) {
-                       $validValid = $element instanceof $elementClass;
-
-                       try {
-                               call_user_func( $function, $list, $element );
-                               $valid = true;
-                       } catch ( InvalidArgumentException $exception ) {
-                               $valid = false;
-                       }
-
-                       $this->assertEquals(
-                               $validValid,
-                               $valid,
-                               'Object of invalid type got successfully added to a GenericArrayObject'
-                       );
-               }
-       }
-
-       /**
-        * @dataProvider elementInstancesProvider
-        *
-        * @since 1.20
-        *
-        * @param array $elements
-        * @covers GenericArrayObject::getObjectType
-        * @covers GenericArrayObject::offsetSet
-        */
-       public function testOffsetSet( array $elements ) {
-               if ( $elements === [] ) {
-                       $this->assertTrue( true );
-
-                       return;
-               }
-
-               $list = $this->getNew();
-
-               $element = reset( $elements );
-               $list->offsetSet( 42, $element );
-               $this->assertEquals( $element, $list->offsetGet( 42 ) );
-
-               $list = $this->getNew();
-
-               $element = reset( $elements );
-               $list['oHai'] = $element;
-               $this->assertEquals( $element, $list['oHai'] );
-
-               $list = $this->getNew();
-
-               $element = reset( $elements );
-               $list->offsetSet( 9001, $element );
-               $this->assertEquals( $element, $list[9001] );
-
-               $list = $this->getNew();
-
-               $element = reset( $elements );
-               $list->offsetSet( null, $element );
-               $this->assertEquals( $element, $list[0] );
-
-               $list = $this->getNew();
-               $offset = 0;
-
-               foreach ( $elements as $element ) {
-                       $list->offsetSet( null, $element );
-                       $this->assertEquals( $element, $list[$offset++] );
-               }
-
-               $this->assertEquals( count( $elements ), $list->count() );
-
-               $this->checkTypeChecks( function ( GenericArrayObject $list, $element ) {
-                       $list->offsetSet( mt_rand(), $element );
-               } );
-       }
-
-       /**
-        * @dataProvider instanceProvider
-        *
-        * @since 1.21
-        *
-        * @param GenericArrayObject $list
-        *
-        * @covers GenericArrayObject::getSerializationData
-        * @covers GenericArrayObject::serialize
-        * @covers GenericArrayObject::unserialize
-        */
-       public function testSerialization( GenericArrayObject $list ) {
-               $serialization = serialize( $list );
-               $copy = unserialize( $serialization );
-
-               $this->assertEquals( $serialization, serialize( $copy ) );
-               $this->assertEquals( count( $list ), count( $copy ) );
-
-               $list = $list->getArrayCopy();
-               $copy = $copy->getArrayCopy();
-
-               $this->assertArrayEquals( $list, $copy, true, true );
-       }
-}
diff --git a/tests/phpunit/includes/libs/HashRingTest.php b/tests/phpunit/includes/libs/HashRingTest.php
deleted file mode 100644 (file)
index acaeb02..0000000
+++ /dev/null
@@ -1,327 +0,0 @@
-<?php
-
-/**
- * @group HashRing
- * @covers HashRing
- */
-class HashRingTest extends PHPUnit\Framework\TestCase {
-
-       use MediaWikiCoversValidator;
-
-       public function testHashRingSerialize() {
-               $map = [ 's1' => 3, 's2' => 10, 's3' => 2, 's4' => 10, 's5' => 2, 's6' => 3 ];
-               $ring = new HashRing( $map, 'md5' );
-
-               $serialized = serialize( $ring );
-               $ringRemade = unserialize( $serialized );
-
-               for ( $i = 0; $i < 100; $i++ ) {
-                       $this->assertEquals(
-                               $ring->getLocation( "hello$i" ),
-                               $ringRemade->getLocation( "hello$i" ),
-                               'Items placed at proper locations'
-                       );
-               }
-       }
-
-       public function testHashRingMapping() {
-               // SHA-1 based and weighted
-               $ring = new HashRing(
-                       [ 's1' => 1, 's2' => 1, 's3' => 2, 's4' => 2, 's5' => 2, 's6' => 3, 's7' => 0 ],
-                       'sha1'
-               );
-
-               $this->assertEquals(
-                       [ 's1' => 1, 's2' => 1, 's3' => 2, 's4' => 2, 's5' => 2, 's6' => 3 ],
-                       $ring->getLocationWeights(),
-                       'Normalized location weights'
-               );
-
-               $locations = [];
-               for ( $i = 0; $i < 25; $i++ ) {
-                       $locations[ "hello$i"] = $ring->getLocation( "hello$i" );
-               }
-               $expectedLocations = [
-                       "hello0" => "s4",
-                       "hello1" => "s6",
-                       "hello2" => "s3",
-                       "hello3" => "s6",
-                       "hello4" => "s6",
-                       "hello5" => "s4",
-                       "hello6" => "s3",
-                       "hello7" => "s4",
-                       "hello8" => "s3",
-                       "hello9" => "s3",
-                       "hello10" => "s3",
-                       "hello11" => "s5",
-                       "hello12" => "s4",
-                       "hello13" => "s5",
-                       "hello14" => "s2",
-                       "hello15" => "s5",
-                       "hello16" => "s6",
-                       "hello17" => "s5",
-                       "hello18" => "s1",
-                       "hello19" => "s1",
-                       "hello20" => "s6",
-                       "hello21" => "s5",
-                       "hello22" => "s3",
-                       "hello23" => "s4",
-                       "hello24" => "s1"
-               ];
-               $this->assertEquals( $expectedLocations, $locations, 'Items placed at proper locations' );
-
-               $locations = [];
-               for ( $i = 0; $i < 5; $i++ ) {
-                       $locations[ "hello$i"] = $ring->getLocations( "hello$i", 2 );
-               }
-
-               $expectedLocations = [
-                       "hello0" => [ "s4", "s5" ],
-                       "hello1" => [ "s6", "s5" ],
-                       "hello2" => [ "s3", "s1" ],
-                       "hello3" => [ "s6", "s5" ],
-                       "hello4" => [ "s6", "s3" ],
-               ];
-               $this->assertEquals( $expectedLocations, $locations, 'Items placed at proper locations' );
-       }
-
-       /**
-        * @dataProvider providor_getHashLocationWeights
-        */
-       public function testHashRingRatios( $locations, $expectedHits ) {
-               $ring = new HashRing( $locations, 'whirlpool' );
-
-               $locationStats = array_fill_keys( array_keys( $locations ), 0 );
-               for ( $i = 0; $i < 10000; ++$i ) {
-                       ++$locationStats[$ring->getLocation( "key-$i" )];
-               }
-               $this->assertEquals( $expectedHits, $locationStats );
-       }
-
-       public static function providor_getHashLocationWeights() {
-               return [
-                       [
-                               [ 'big' => 10, 'medium' => 5, 'small' => 1 ],
-                               [ 'big' => 6037, 'medium' => 3314, 'small' => 649 ]
-                       ]
-               ];
-       }
-
-       /**
-        * @dataProvider providor_getHashLocationWeights2
-        */
-       public function testHashRingRatios2( $locations, $expected ) {
-               $ring = new HashRing( $locations, 'sha1' );
-               $locationStats = array_fill_keys( array_keys( $locations ), 0 );
-               for ( $i = 0; $i < 1000; ++$i ) {
-                       foreach ( $ring->getLocations( "key-$i", 3 ) as $location ) {
-                               ++$locationStats[$location];
-                       }
-               }
-               $this->assertEquals( $expected, $locationStats );
-       }
-
-       public static function providor_getHashLocationWeights2() {
-               return [
-                       [
-                               [ 'big1' => 10, 'big2' => 10, 'big3' => 10, 'small1' => 1, 'small2' => 1 ],
-                               [ 'big1' => 929, 'big2' => 899, 'big3' => 887, 'small1' => 143, 'small2' => 142 ]
-                       ]
-               ];
-       }
-
-       public function testHashRingEjection() {
-               $map = [ 's1' => 5, 's2' => 5, 's3' => 10, 's4' => 10, 's5' => 5, 's6' => 5 ];
-               $ring = new HashRing( $map, 'md5' );
-
-               $ring->ejectFromLiveRing( 's3', 30 );
-               $ring->ejectFromLiveRing( 's6', 15 );
-
-               $this->assertEquals(
-                       [ 's1' => 5, 's2' => 5, 's4' => 10, 's5' => 5 ],
-                       $ring->getLiveLocationWeights(),
-                       'Live location weights'
-               );
-
-               for ( $i = 0; $i < 100; ++$i ) {
-                       $key = "key-$i";
-
-                       $this->assertNotEquals( 's3', $ring->getLiveLocation( $key ), 'ejected' );
-                       $this->assertNotEquals( 's6', $ring->getLiveLocation( $key ), 'ejected' );
-
-                       if ( !in_array( $ring->getLocation( $key ), [ 's3', 's6' ], true ) ) {
-                               $this->assertEquals(
-                                       $ring->getLocation( $key ),
-                                       $ring->getLiveLocation( $key ),
-                                       "Live ring otherwise matches (#$i)"
-                               );
-                               $this->assertEquals(
-                                       $ring->getLocations( $key, 1 ),
-                                       $ring->getLiveLocations( $key, 1 ),
-                                       "Live ring otherwise matches (#$i)"
-                               );
-                       }
-               }
-       }
-
-       public function testHashRingCollision() {
-               $ring1 = new HashRing( [ 0 => 1, 6497 => 1 ] );
-               $ring2 = new HashRing( [ 6497 => 1, 0 => 1 ] );
-
-               for ( $i = 0; $i < 100; ++$i ) {
-                       $this->assertEquals( $ring1->getLocation( $i ), $ring2->getLocation( $i ) );
-               }
-       }
-
-       public function testHashRingKetamaMode() {
-               // Same as https://github.com/RJ/ketama/blob/master/ketama.servers
-               $map = [
-                       '10.0.1.1:11211' => 600,
-                       '10.0.1.2:11211' => 300,
-                       '10.0.1.3:11211' => 200,
-                       '10.0.1.4:11211' => 350,
-                       '10.0.1.5:11211' => 1000,
-                       '10.0.1.6:11211' => 800,
-                       '10.0.1.7:11211' => 950,
-                       '10.0.1.8:11211' => 100
-               ];
-               $ring = new HashRing( $map, 'md5' );
-               $wrapper = \Wikimedia\TestingAccessWrapper::newFromObject( $ring );
-
-               $ketama_test = function ( $count ) use ( $wrapper ) {
-                       $baseRing = $wrapper->baseRing;
-
-                       $lines = [];
-                       for ( $key = 0; $key < $count; ++$key ) {
-                               $location = $wrapper->getLocation( $key );
-
-                               $itemPos = $wrapper->getItemPosition( $key );
-                               $nodeIndex = $wrapper->findNodeIndexForPosition( $itemPos, $baseRing );
-                               $nodePos = $baseRing[$nodeIndex][HashRing::KEY_POS];
-
-                               $lines[] = sprintf( "%u %u %s\n", $itemPos, $nodePos, $location );
-                       }
-
-                       return "\n" . implode( '', $lines );
-               };
-
-               // Known correct values generated from C code:
-               // https://github.com/RJ/ketama/blob/master/libketama/ketama_test.c
-               $expected = <<<EOT
-
-2216742351 2217271743 10.0.1.1:11211
-943901380 949045552 10.0.1.5:11211
-2373066440 2374693370 10.0.1.6:11211
-2127088620 2130338203 10.0.1.6:11211
-2046197672 2051996197 10.0.1.7:11211
-2134629092 2135172435 10.0.1.1:11211
-470382870 472541453 10.0.1.7:11211
-1608782991 1609789509 10.0.1.3:11211
-2516119753 2520092206 10.0.1.2:11211
-3465331781 3466294492 10.0.1.4:11211
-1749342675 1753760600 10.0.1.5:11211
-1136464485 1137779711 10.0.1.1:11211
-3620997826 3621580689 10.0.1.7:11211
-283385029 285581365 10.0.1.6:11211
-2300818346 2302165654 10.0.1.5:11211
-2132603803 2134614475 10.0.1.8:11211
-2962705863 2969767984 10.0.1.2:11211
-786427760 786565633 10.0.1.5:11211
-4095887727 4096760944 10.0.1.6:11211
-2906459679 2906987515 10.0.1.6:11211
-137884056 138922607 10.0.1.4:11211
-81549628 82491298 10.0.1.6:11211
-3530020790 3530525869 10.0.1.6:11211
-4231817527 4234960467 10.0.1.7:11211
-2011099423 2014738083 10.0.1.7:11211
-107620750 120968799 10.0.1.6:11211
-3979113294 3981926993 10.0.1.4:11211
-273671938 276355738 10.0.1.4:11211
-4032816947 4033300359 10.0.1.5:11211
-464234862 466093615 10.0.1.1:11211
-3007059764 3007671127 10.0.1.5:11211
-542337729 542491760 10.0.1.7:11211
-4040385635 4044064727 10.0.1.5:11211
-3319802648 3320661601 10.0.1.7:11211
-1032153571 1035085391 10.0.1.1:11211
-3543939100 3545608820 10.0.1.5:11211
-3876899353 3885324049 10.0.1.2:11211
-3771318181 3773259708 10.0.1.8:11211
-3457906597 3459285639 10.0.1.5:11211
-3028975062 3031083168 10.0.1.7:11211
-244467158 250943416 10.0.1.5:11211
-1604785716 1609789509 10.0.1.3:11211
-3905343649 3905751132 10.0.1.1:11211
-1713497623 1725056963 10.0.1.5:11211
-1668356087 1668827816 10.0.1.5:11211
-3427369836 3438933308 10.0.1.1:11211
-2515850457 2520092206 10.0.1.2:11211
-3886138983 3887390208 10.0.1.1:11211
-4019334756 4023153300 10.0.1.8:11211
-1170561012 1170785765 10.0.1.7:11211
-1841809344 1848425105 10.0.1.6:11211
-973223976 973369204 10.0.1.1:11211
-358093210 359562433 10.0.1.6:11211
-378350808 380841931 10.0.1.5:11211
-4008477862 4012085095 10.0.1.7:11211
-1027226549 1028630030 10.0.1.6:11211
-2386583967 2387706118 10.0.1.1:11211
-522892146 524831677 10.0.1.7:11211
-3779194982 3788912803 10.0.1.5:11211
-3764731657 3771312500 10.0.1.7:11211
-184756999 187529415 10.0.1.6:11211
-838351231 845886003 10.0.1.3:11211
-2827220548 2828019973 10.0.1.6:11211
-3604721411 3607668249 10.0.1.6:11211
-472866282 475506254 10.0.1.5:11211
-2752268796 2754833471 10.0.1.5:11211
-1791464754 1795042583 10.0.1.7:11211
-3029359475 3031083168 10.0.1.7:11211
-3633378211 3639985542 10.0.1.6:11211
-3148267284 3149217023 10.0.1.6:11211
-163887996 166705043 10.0.1.7:11211
-3642803426 3649125922 10.0.1.7:11211
-3901799218 3902199881 10.0.1.7:11211
-418045394 425867331 10.0.1.6:11211
-346775981 348578169 10.0.1.6:11211
-368352208 372224616 10.0.1.7:11211
-2643711995 2644259911 10.0.1.5:11211
-2032983336 2033860601 10.0.1.6:11211
-3567842357 3572867530 10.0.1.2:11211
-1024982737 1028630030 10.0.1.6:11211
-933966832 938106828 10.0.1.7:11211
-2102520899 2103402846 10.0.1.7:11211
-3537205399 3538094881 10.0.1.7:11211
-2311233534 2314593262 10.0.1.1:11211
-2500514664 2503565236 10.0.1.7:11211
-1091958846 1093484995 10.0.1.6:11211
-3984972691 3987453644 10.0.1.1:11211
-2669994439 2670911201 10.0.1.4:11211
-2846111786 2846115813 10.0.1.5:11211
-1805010806 1808593732 10.0.1.8:11211
-1587024774 1587746378 10.0.1.5:11211
-3214549588 3215619351 10.0.1.2:11211
-1965214866 1970922428 10.0.1.7:11211
-1038671000 1040777775 10.0.1.7:11211
-820820468 823114475 10.0.1.6:11211
-2722835329 2723166435 10.0.1.5:11211
-1602053414 1604196066 10.0.1.5:11211
-1330835426 1335097278 10.0.1.5:11211
-556547565 557075710 10.0.1.4:11211
-2977587884 2978402952 10.0.1.1:11211
-
-EOT;
-
-               $this->assertEquals( $expected, $ketama_test( 100 ), 'Ketama mode (diff check)' );
-
-               // Hash of known correct values from C code
-               $this->assertEquals(
-                       'c69ac9eb7a8a630c0cded201cefeaace',
-                       md5( $ketama_test( 1e5 ) ),
-                       'Ketama mode (large, MD5 check)'
-               );
-
-               // Slower, full upstream MD5 check, manually verified 3/21/2018
-               // $this->assertEquals( '5672b131391f5aa2b280936aec1eea74', md5( $ketama_test( 1e6 ) ) );
-       }
-}
diff --git a/tests/phpunit/includes/libs/HtmlArmorTest.php b/tests/phpunit/includes/libs/HtmlArmorTest.php
deleted file mode 100644 (file)
index c5e87e4..0000000
+++ /dev/null
@@ -1,55 +0,0 @@
-<?php
-
-/**
- * @covers HtmlArmor
- */
-class HtmlArmorTest extends PHPUnit\Framework\TestCase {
-
-       use MediaWikiCoversValidator;
-
-       public static function provideConstructor() {
-               return [
-                       [ 'test' ],
-                       [ null ],
-                       [ '<em>some html!</em>' ]
-               ];
-       }
-
-       /**
-        * @dataProvider provideConstructor
-        */
-       public function testConstructor( $value ) {
-               $this->assertInstanceOf( HtmlArmor::class, new HtmlArmor( $value ) );
-       }
-
-       public static function provideGetHtml() {
-               return [
-                       [
-                               'foobar',
-                               'foobar',
-                       ],
-                       [
-                               '<script>alert("evil!");</script>',
-                               '&lt;script&gt;alert(&quot;evil!&quot;);&lt;/script&gt;',
-                       ],
-                       [
-                               new HtmlArmor( '<script>alert("evil!");</script>' ),
-                               '<script>alert("evil!");</script>',
-                       ],
-                       [
-                               new HtmlArmor( null ),
-                               null,
-                       ]
-               ];
-       }
-
-       /**
-        * @dataProvider provideGetHtml
-        */
-       public function testGetHtml( $input, $expected ) {
-               $this->assertEquals(
-                       $expected,
-                       HtmlArmor::getHtml( $input )
-               );
-       }
-}
diff --git a/tests/phpunit/includes/libs/IEUrlExtensionTest.php b/tests/phpunit/includes/libs/IEUrlExtensionTest.php
deleted file mode 100644 (file)
index e04b2e2..0000000
+++ /dev/null
@@ -1,42 +0,0 @@
-<?php
-
-class IEUrlExtensionTest extends PHPUnit\Framework\TestCase {
-
-       use MediaWikiCoversValidator;
-
-       public function provideFindIE6Extension() {
-               return [
-                       // url, expected, message
-                       [ 'x.y', 'y', 'Simple extension' ],
-                       [ 'x', '', 'No extension' ],
-                       [ '', '', 'Empty string' ],
-                       [ '?', '', 'Question mark only' ],
-                       [ '.x?', 'x', 'Extension then question mark' ],
-                       [ '?.x', 'x', 'Question mark then extension' ],
-                       [ '.x*', '', 'Extension with invalid character' ],
-                       [ '*.x', 'x', 'Invalid character followed by an extension' ],
-                       [ 'a?b?.c?.d?e?f', 'c', 'Multiple question marks' ],
-                       [ 'a?b?.exe?.d?.e', 'd', '.exe exception' ],
-                       [ 'a?b?.exe', 'exe', '.exe exception 2' ],
-                       [ 'a#b.c', '', 'Hash character preceding extension' ],
-                       [ 'a?#b.c', '', 'Hash character preceding extension 2' ],
-                       [ '.', '', 'Dot at end of string' ],
-                       [ 'x.y.z', 'z', 'Two dots' ],
-                       [ 'example.php?foo=a&bar=b', 'php', 'Script with query' ],
-                       [ 'example%2Ephp?foo=a&bar=b', '', 'Script with urlencoded dot and query' ],
-                       [ 'example%2Ephp?foo=a.x&bar=b.y', 'y', 'Script with urlencoded dot and query with dot' ],
-               ];
-       }
-
-       /**
-        * @covers IEUrlExtension::findIE6Extension
-        * @dataProvider provideFindIE6Extension
-        */
-       public function testFindIE6Extension( $url, $expected, $message ) {
-               $this->assertEquals(
-                       $expected,
-                       IEUrlExtension::findIE6Extension( $url ),
-                       $message
-               );
-       }
-}
diff --git a/tests/phpunit/includes/libs/IPTest.php b/tests/phpunit/includes/libs/IPTest.php
deleted file mode 100644 (file)
index 9ec53c0..0000000
+++ /dev/null
@@ -1,673 +0,0 @@
-<?php
-/**
- * Tests for IP validity functions.
- *
- * Ported from /t/inc/IP.t by avar.
- *
- * @group IP
- * @todo Test methods in this call should be split into a method and a
- * dataprovider.
- */
-class IPTest extends PHPUnit\Framework\TestCase {
-
-       use MediaWikiCoversValidator;
-
-       /**
-        * @covers IP::isIPAddress
-        * @dataProvider provideInvalidIPs
-        */
-       public function testIsNotIPAddress( $val, $desc ) {
-               $this->assertFalse( IP::isIPAddress( $val ), $desc );
-       }
-
-       /**
-        * Provide a list of things that aren't IP addresses
-        */
-       public function provideInvalidIPs() {
-               return [
-                       [ false, 'Boolean false is not an IP' ],
-                       [ true, 'Boolean true is not an IP' ],
-                       [ '', 'Empty string is not an IP' ],
-                       [ 'abc', 'Garbage IP string' ],
-                       [ ':', 'Single ":" is not an IP' ],
-                       [ '2001:0DB8::A:1::1', 'IPv6 with a double :: occurrence' ],
-                       [ '2001:0DB8::A:1::', 'IPv6 with a double :: occurrence, last at end' ],
-                       [ '::2001:0DB8::5:1', 'IPv6 with a double :: occurrence, firt at beginning' ],
-                       [ '124.24.52', 'IPv4 not enough quads' ],
-                       [ '24.324.52.13', 'IPv4 out of range' ],
-                       [ '.24.52.13', 'IPv4 starts with period' ],
-                       [ 'fc:100:300', 'IPv6 with only 3 words' ],
-               ];
-       }
-
-       /**
-        * @covers IP::isIPAddress
-        */
-       public function testisIPAddress() {
-               $this->assertTrue( IP::isIPAddress( '::' ), 'RFC 4291 IPv6 Unspecified Address' );
-               $this->assertTrue( IP::isIPAddress( '::1' ), 'RFC 4291 IPv6 Loopback Address' );
-               $this->assertTrue( IP::isIPAddress( '74.24.52.13/20' ), 'IPv4 range' );
-               $this->assertTrue( IP::isIPAddress( 'fc:100:a:d:1:e:ac:0/24' ), 'IPv6 range' );
-               $this->assertTrue( IP::isIPAddress( 'fc::100:a:d:1:e:ac/96' ), 'IPv6 range with "::"' );
-
-               $validIPs = [ 'fc:100::', 'fc:100:a:d:1:e:ac::', 'fc::100', '::fc:100:a:d:1:e:ac',
-                       '::fc', 'fc::100:a:d:1:e:ac', 'fc:100:a:d:1:e:ac:0', '124.24.52.13', '1.24.52.13' ];
-               foreach ( $validIPs as $ip ) {
-                       $this->assertTrue( IP::isIPAddress( $ip ), "$ip is a valid IP address" );
-               }
-       }
-
-       /**
-        * @covers IP::isIPv6
-        */
-       public function testisIPv6() {
-               $this->assertFalse( IP::isIPv6( ':fc:100::' ), 'IPv6 starting with lone ":"' );
-               $this->assertFalse( IP::isIPv6( 'fc:100:::' ), 'IPv6 ending with a ":::"' );
-               $this->assertFalse( IP::isIPv6( 'fc:300' ), 'IPv6 with only 2 words' );
-               $this->assertFalse( IP::isIPv6( 'fc:100:300' ), 'IPv6 with only 3 words' );
-
-               $this->assertTrue( IP::isIPv6( 'fc:100::' ) );
-               $this->assertTrue( IP::isIPv6( 'fc:100:a::' ) );
-               $this->assertTrue( IP::isIPv6( 'fc:100:a:d::' ) );
-               $this->assertTrue( IP::isIPv6( 'fc:100:a:d:1::' ) );
-               $this->assertTrue( IP::isIPv6( 'fc:100:a:d:1:e::' ) );
-               $this->assertTrue( IP::isIPv6( 'fc:100:a:d:1:e:ac::' ) );
-
-               $this->assertFalse( IP::isIPv6( 'fc:100:a:d:1:e:ac:0::' ), 'IPv6 with 8 words ending with "::"' );
-               $this->assertFalse(
-                       IP::isIPv6( 'fc:100:a:d:1:e:ac:0:1::' ),
-                       'IPv6 with 9 words ending with "::"'
-               );
-
-               $this->assertFalse( IP::isIPv6( ':::' ) );
-               $this->assertFalse( IP::isIPv6( '::0:' ), 'IPv6 ending in a lone ":"' );
-
-               $this->assertTrue( IP::isIPv6( '::' ), 'IPv6 zero address' );
-               $this->assertTrue( IP::isIPv6( '::0' ) );
-               $this->assertTrue( IP::isIPv6( '::fc' ) );
-               $this->assertTrue( IP::isIPv6( '::fc:100' ) );
-               $this->assertTrue( IP::isIPv6( '::fc:100:a' ) );
-               $this->assertTrue( IP::isIPv6( '::fc:100:a:d' ) );
-               $this->assertTrue( IP::isIPv6( '::fc:100:a:d:1' ) );
-               $this->assertTrue( IP::isIPv6( '::fc:100:a:d:1:e' ) );
-               $this->assertTrue( IP::isIPv6( '::fc:100:a:d:1:e:ac' ) );
-
-               $this->assertFalse( IP::isIPv6( '::fc:100:a:d:1:e:ac:0' ), 'IPv6 with "::" and 8 words' );
-               $this->assertFalse( IP::isIPv6( '::fc:100:a:d:1:e:ac:0:1' ), 'IPv6 with 9 words' );
-
-               $this->assertFalse( IP::isIPv6( ':fc::100' ), 'IPv6 starting with lone ":"' );
-               $this->assertFalse( IP::isIPv6( 'fc::100:' ), 'IPv6 ending with lone ":"' );
-               $this->assertFalse( IP::isIPv6( 'fc:::100' ), 'IPv6 with ":::" in the middle' );
-
-               $this->assertTrue( IP::isIPv6( 'fc::100' ), 'IPv6 with "::" and 2 words' );
-               $this->assertTrue( IP::isIPv6( 'fc::100:a' ), 'IPv6 with "::" and 3 words' );
-               $this->assertTrue( IP::isIPv6( 'fc::100:a:d' ), 'IPv6 with "::" and 4 words' );
-               $this->assertTrue( IP::isIPv6( 'fc::100:a:d:1' ), 'IPv6 with "::" and 5 words' );
-               $this->assertTrue( IP::isIPv6( 'fc::100:a:d:1:e' ), 'IPv6 with "::" and 6 words' );
-               $this->assertTrue( IP::isIPv6( 'fc::100:a:d:1:e:ac' ), 'IPv6 with "::" and 7 words' );
-               $this->assertTrue( IP::isIPv6( '2001::df' ), 'IPv6 with "::" and 2 words' );
-               $this->assertTrue( IP::isIPv6( '2001:5c0:1400:a::df' ), 'IPv6 with "::" and 5 words' );
-               $this->assertTrue( IP::isIPv6( '2001:5c0:1400:a::df:2' ), 'IPv6 with "::" and 6 words' );
-
-               $this->assertFalse( IP::isIPv6( 'fc::100:a:d:1:e:ac:0' ), 'IPv6 with "::" and 8 words' );
-               $this->assertFalse( IP::isIPv6( 'fc::100:a:d:1:e:ac:0:1' ), 'IPv6 with 9 words' );
-
-               $this->assertTrue( IP::isIPv6( 'fc:100:a:d:1:e:ac:0' ) );
-       }
-
-       /**
-        * @covers IP::isIPv4
-        * @dataProvider provideInvalidIPv4Addresses
-        */
-       public function testisNotIPv4( $bogusIP, $desc ) {
-               $this->assertFalse( IP::isIPv4( $bogusIP ), $desc );
-       }
-
-       public function provideInvalidIPv4Addresses() {
-               return [
-                       [ false, 'Boolean false is not an IP' ],
-                       [ true, 'Boolean true is not an IP' ],
-                       [ '', 'Empty string is not an IP' ],
-                       [ 'abc', 'Letters are not an IP' ],
-                       [ ':', 'A colon is not an IP' ],
-                       [ '124.24.52', 'IPv4 not enough quads' ],
-                       [ '24.324.52.13', 'IPv4 out of range' ],
-                       [ '.24.52.13', 'IPv4 starts with period' ],
-               ];
-       }
-
-       /**
-        * @covers IP::isIPv4
-        * @dataProvider provideValidIPv4Address
-        */
-       public function testIsIPv4( $ip, $desc ) {
-               $this->assertTrue( IP::isIPv4( $ip ), $desc );
-       }
-
-       /**
-        * Provide some IPv4 addresses and ranges
-        */
-       public function provideValidIPv4Address() {
-               return [
-                       [ '124.24.52.13', 'Valid IPv4 address' ],
-                       [ '1.24.52.13', 'Another valid IPv4 address' ],
-                       [ '74.24.52.13/20', 'An IPv4 range' ],
-               ];
-       }
-
-       /**
-        * @covers IP::isValid
-        */
-       public function testValidIPs() {
-               foreach ( range( 0, 255 ) as $i ) {
-                       $a = sprintf( "%03d", $i );
-                       $b = sprintf( "%02d", $i );
-                       $c = sprintf( "%01d", $i );
-                       foreach ( array_unique( [ $a, $b, $c ] ) as $f ) {
-                               $ip = "$f.$f.$f.$f";
-                               $this->assertTrue( IP::isValid( $ip ), "$ip is a valid IPv4 address" );
-                       }
-               }
-               foreach ( range( 0x0, 0xFFFF, 0xF ) as $i ) {
-                       $a = sprintf( "%04x", $i );
-                       $b = sprintf( "%03x", $i );
-                       $c = sprintf( "%02x", $i );
-                       foreach ( array_unique( [ $a, $b, $c ] ) as $f ) {
-                               $ip = "$f:$f:$f:$f:$f:$f:$f:$f";
-                               $this->assertTrue( IP::isValid( $ip ), "$ip is a valid IPv6 address" );
-                       }
-               }
-               // test with some abbreviations
-               $this->assertFalse( IP::isValid( ':fc:100::' ), 'IPv6 starting with lone ":"' );
-               $this->assertFalse( IP::isValid( 'fc:100:::' ), 'IPv6 ending with a ":::"' );
-               $this->assertFalse( IP::isValid( 'fc:300' ), 'IPv6 with only 2 words' );
-               $this->assertFalse( IP::isValid( 'fc:100:300' ), 'IPv6 with only 3 words' );
-
-               $this->assertTrue( IP::isValid( 'fc:100::' ) );
-               $this->assertTrue( IP::isValid( 'fc:100:a:d:1:e::' ) );
-               $this->assertTrue( IP::isValid( 'fc:100:a:d:1:e:ac::' ) );
-
-               $this->assertTrue( IP::isValid( 'fc::100' ), 'IPv6 with "::" and 2 words' );
-               $this->assertTrue( IP::isValid( 'fc::100:a' ), 'IPv6 with "::" and 3 words' );
-               $this->assertTrue( IP::isValid( '2001::df' ), 'IPv6 with "::" and 2 words' );
-               $this->assertTrue( IP::isValid( '2001:5c0:1400:a::df' ), 'IPv6 with "::" and 5 words' );
-               $this->assertTrue( IP::isValid( '2001:5c0:1400:a::df:2' ), 'IPv6 with "::" and 6 words' );
-               $this->assertTrue( IP::isValid( 'fc::100:a:d:1' ), 'IPv6 with "::" and 5 words' );
-               $this->assertTrue( IP::isValid( 'fc::100:a:d:1:e:ac' ), 'IPv6 with "::" and 7 words' );
-
-               $this->assertFalse(
-                       IP::isValid( 'fc:100:a:d:1:e:ac:0::' ),
-                       'IPv6 with 8 words ending with "::"'
-               );
-               $this->assertFalse(
-                       IP::isValid( 'fc:100:a:d:1:e:ac:0:1::' ),
-                       'IPv6 with 9 words ending with "::"'
-               );
-       }
-
-       /**
-        * @covers IP::isValid
-        */
-       public function testInvalidIPs() {
-               // Out of range...
-               foreach ( range( 256, 999 ) as $i ) {
-                       $a = sprintf( "%03d", $i );
-                       $b = sprintf( "%02d", $i );
-                       $c = sprintf( "%01d", $i );
-                       foreach ( array_unique( [ $a, $b, $c ] ) as $f ) {
-                               $ip = "$f.$f.$f.$f";
-                               $this->assertFalse( IP::isValid( $ip ), "$ip is not a valid IPv4 address" );
-                       }
-               }
-               foreach ( range( 'g', 'z' ) as $i ) {
-                       $a = sprintf( "%04s", $i );
-                       $b = sprintf( "%03s", $i );
-                       $c = sprintf( "%02s", $i );
-                       foreach ( array_unique( [ $a, $b, $c ] ) as $f ) {
-                               $ip = "$f:$f:$f:$f:$f:$f:$f:$f";
-                               $this->assertFalse( IP::isValid( $ip ), "$ip is not a valid IPv6 address" );
-                       }
-               }
-               // Have CIDR
-               $ipCIDRs = [
-                       '212.35.31.121/32',
-                       '212.35.31.121/18',
-                       '212.35.31.121/24',
-                       '::ff:d:321:5/96',
-                       'ff::d3:321:5/116',
-                       'c:ff:12:1:ea:d:321:5/120',
-               ];
-               foreach ( $ipCIDRs as $i ) {
-                       $this->assertFalse( IP::isValid( $i ),
-                               "$i is an invalid IP address because it is a range" );
-               }
-               // Incomplete/garbage
-               $invalid = [
-                       'www.xn--var-xla.net',
-                       '216.17.184.G',
-                       '216.17.184.1.',
-                       '216.17.184',
-                       '216.17.184.',
-                       '256.17.184.1'
-               ];
-               foreach ( $invalid as $i ) {
-                       $this->assertFalse( IP::isValid( $i ), "$i is an invalid IP address" );
-               }
-       }
-
-       /**
-        * Provide some valid IP ranges
-        */
-       public function provideValidRanges() {
-               return [
-                       [ '116.17.184.5/32' ],
-                       [ '0.17.184.5/30' ],
-                       [ '16.17.184.1/24' ],
-                       [ '30.242.52.14/1' ],
-                       [ '10.232.52.13/8' ],
-                       [ '30.242.52.14/0' ],
-                       [ '::e:f:2001/96' ],
-                       [ '::c:f:2001/128' ],
-                       [ '::10:f:2001/70' ],
-                       [ '::fe:f:2001/1' ],
-                       [ '::6d:f:2001/8' ],
-                       [ '::fe:f:2001/0' ],
-               ];
-       }
-
-       /**
-        * @covers IP::isValidRange
-        * @dataProvider provideValidRanges
-        */
-       public function testValidRanges( $range ) {
-               $this->assertTrue( IP::isValidRange( $range ), "$range is a valid IP range" );
-       }
-
-       /**
-        * @covers IP::isValidRange
-        * @dataProvider provideInvalidRanges
-        */
-       public function testInvalidRanges( $invalid ) {
-               $this->assertFalse( IP::isValidRange( $invalid ), "$invalid is not a valid IP range" );
-       }
-
-       public function provideInvalidRanges() {
-               return [
-                       [ '116.17.184.5/33' ],
-                       [ '0.17.184.5/130' ],
-                       [ '16.17.184.1/-1' ],
-                       [ '10.232.52.13/*' ],
-                       [ '7.232.52.13/ab' ],
-                       [ '11.232.52.13/' ],
-                       [ '::e:f:2001/129' ],
-                       [ '::c:f:2001/228' ],
-                       [ '::10:f:2001/-1' ],
-                       [ '::6d:f:2001/*' ],
-                       [ '::86:f:2001/ab' ],
-                       [ '::23:f:2001/' ],
-               ];
-       }
-
-       /**
-        * @covers IP::sanitizeIP
-        * @dataProvider provideSanitizeIP
-        */
-       public function testSanitizeIP( $expected, $input ) {
-               $result = IP::sanitizeIP( $input );
-               $this->assertEquals( $expected, $result );
-       }
-
-       /**
-        * Provider for IP::testSanitizeIP()
-        */
-       public static function provideSanitizeIP() {
-               return [
-                       [ '0.0.0.0', '0.0.0.0' ],
-                       [ '0.0.0.0', '00.00.00.00' ],
-                       [ '0.0.0.0', '000.000.000.000' ],
-                       [ '0.0.0.0/24', '000.000.000.000/24' ],
-                       [ '141.0.11.253', '141.000.011.253' ],
-                       [ '1.2.4.5', '1.2.4.5' ],
-                       [ '1.2.4.5', '01.02.04.05' ],
-                       [ '1.2.4.5', '001.002.004.005' ],
-                       [ '10.0.0.1', '010.0.000.1' ],
-                       [ '80.72.250.4', '080.072.250.04' ],
-                       [ 'Foo.1000.00', 'Foo.1000.00' ],
-                       [ 'Bar.01', 'Bar.01' ],
-                       [ 'Bar.010', 'Bar.010' ],
-                       [ null, '' ],
-                       [ null, ' ' ]
-               ];
-       }
-
-       /**
-        * @covers IP::toHex
-        * @dataProvider provideToHex
-        */
-       public function testToHex( $expected, $input ) {
-               $result = IP::toHex( $input );
-               $this->assertTrue( $result === false || is_string( $result ) );
-               $this->assertEquals( $expected, $result );
-       }
-
-       /**
-        * Provider for IP::testToHex()
-        */
-       public static function provideToHex() {
-               return [
-                       [ '00000001', '0.0.0.1' ],
-                       [ '01020304', '1.2.3.4' ],
-                       [ '7F000001', '127.0.0.1' ],
-                       [ '80000000', '128.0.0.0' ],
-                       [ 'DEADCAFE', '222.173.202.254' ],
-                       [ 'FFFFFFFF', '255.255.255.255' ],
-                       [ '8D000BFD', '141.000.11.253' ],
-                       [ false, 'IN.VA.LI.D' ],
-                       [ 'v6-00000000000000000000000000000001', '::1' ],
-                       [ 'v6-20010DB885A3000000008A2E03707334', '2001:0db8:85a3:0000:0000:8a2e:0370:7334' ],
-                       [ 'v6-20010DB885A3000000008A2E03707334', '2001:db8:85a3::8a2e:0370:7334' ],
-                       [ false, 'IN:VA::LI:D' ],
-                       [ false, ':::1' ]
-               ];
-       }
-
-       /**
-        * @covers IP::isPublic
-        * @dataProvider provideIsPublic
-        */
-       public function testIsPublic( $expected, $input ) {
-               $result = IP::isPublic( $input );
-               $this->assertEquals( $expected, $result );
-       }
-
-       /**
-        * Provider for IP::testIsPublic()
-        */
-       public static function provideIsPublic() {
-               return [
-                       [ false, 'fc00::3' ], # RFC 4193 (local)
-                       [ false, 'fc00::ff' ], # RFC 4193 (local)
-                       [ false, '127.1.2.3' ], # loopback
-                       [ false, '::1' ], # loopback
-                       [ false, 'fe80::1' ], # link-local
-                       [ false, '169.254.1.1' ], # link-local
-                       [ false, '10.0.0.1' ], # RFC 1918 (private)
-                       [ false, '172.16.0.1' ], # RFC 1918 (private)
-                       [ false, '192.168.0.1' ], # RFC 1918 (private)
-                       [ true, '2001:5c0:1000:a::133' ], # public
-                       [ true, 'fc::3' ], # public
-                       [ true, '00FC::' ] # public
-               ];
-       }
-
-       // Private wrapper used to test CIDR Parsing.
-       private function assertFalseCIDR( $CIDR, $msg = '' ) {
-               $ff = [ false, false ];
-               $this->assertEquals( $ff, IP::parseCIDR( $CIDR ), $msg );
-       }
-
-       // Private wrapper to test network shifting using only dot notation
-       private function assertNet( $expected, $CIDR ) {
-               $parse = IP::parseCIDR( $CIDR );
-               $this->assertEquals( $expected, long2ip( $parse[0] ), "network shifting $CIDR" );
-       }
-
-       /**
-        * @covers IP::hexToQuad
-        * @dataProvider provideIPsAndHexes
-        */
-       public function testHexToQuad( $ip, $hex ) {
-               $this->assertEquals( $ip, IP::hexToQuad( $hex ) );
-       }
-
-       /**
-        * Provide some IP addresses and their equivalent hex representations
-        */
-       public function provideIPsandHexes() {
-               return [
-                       [ '0.0.0.1', '00000001' ],
-                       [ '255.0.0.0', 'FF000000' ],
-                       [ '255.255.255.255', 'FFFFFFFF' ],
-                       [ '10.188.222.255', '0ABCDEFF' ],
-                       // hex not left-padded...
-                       [ '0.0.0.0', '0' ],
-                       [ '0.0.0.1', '1' ],
-                       [ '0.0.0.255', 'FF' ],
-                       [ '0.0.255.0', 'FF00' ],
-               ];
-       }
-
-       /**
-        * @covers IP::hexToOctet
-        * @dataProvider provideOctetsAndHexes
-        */
-       public function testHexToOctet( $octet, $hex ) {
-               $this->assertEquals( $octet, IP::hexToOctet( $hex ) );
-       }
-
-       /**
-        * Provide some hex and octet representations of the same IPs
-        */
-       public function provideOctetsAndHexes() {
-               return [
-                       [ '0:0:0:0:0:0:0:1', '00000000000000000000000000000001' ],
-                       [ '0:0:0:0:0:0:FF:3', '00000000000000000000000000FF0003' ],
-                       [ '0:0:0:0:0:0:FF00:6', '000000000000000000000000FF000006' ],
-                       [ '0:0:0:0:0:0:FCCF:FAFF', '000000000000000000000000FCCFFAFF' ],
-                       [ 'FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF', 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF' ],
-                       // hex not left-padded...
-                       [ '0:0:0:0:0:0:0:0', '0' ],
-                       [ '0:0:0:0:0:0:0:1', '1' ],
-                       [ '0:0:0:0:0:0:0:FF', 'FF' ],
-                       [ '0:0:0:0:0:0:0:FFD0', 'FFD0' ],
-                       [ '0:0:0:0:0:0:FA00:0', 'FA000000' ],
-                       [ '0:0:0:0:0:0:FCCF:FAFF', 'FCCFFAFF' ],
-               ];
-       }
-
-       /**
-        * IP::parseCIDR() returns an array containing a signed IP address
-        * representing the network mask and the bit mask.
-        * @covers IP::parseCIDR
-        */
-       public function testCIDRParsing() {
-               $this->assertFalseCIDR( '192.0.2.0', "missing mask" );
-               $this->assertFalseCIDR( '192.0.2.0/', "missing bitmask" );
-
-               // Verify if statement
-               $this->assertFalseCIDR( '256.0.0.0/32', "invalid net" );
-               $this->assertFalseCIDR( '192.0.2.0/AA', "mask not numeric" );
-               $this->assertFalseCIDR( '192.0.2.0/-1', "mask < 0" );
-               $this->assertFalseCIDR( '192.0.2.0/33', "mask > 32" );
-
-               // Check internal logic
-               # 0 mask always result in array(0,0)
-               $this->assertEquals( [ 0, 0 ], IP::parseCIDR( '192.0.0.2/0' ) );
-               $this->assertEquals( [ 0, 0 ], IP::parseCIDR( '0.0.0.0/0' ) );
-               $this->assertEquals( [ 0, 0 ], IP::parseCIDR( '255.255.255.255/0' ) );
-
-               // @todo FIXME: Add more tests.
-
-               # This part test network shifting
-               $this->assertNet( '192.0.0.0', '192.0.0.2/24' );
-               $this->assertNet( '192.168.5.0', '192.168.5.13/24' );
-               $this->assertNet( '10.0.0.160', '10.0.0.161/28' );
-               $this->assertNet( '10.0.0.0', '10.0.0.3/28' );
-               $this->assertNet( '10.0.0.0', '10.0.0.3/30' );
-               $this->assertNet( '10.0.0.4', '10.0.0.4/30' );
-               $this->assertNet( '172.17.32.0', '172.17.35.48/21' );
-               $this->assertNet( '10.128.0.0', '10.135.0.0/9' );
-               $this->assertNet( '134.0.0.0', '134.0.5.1/8' );
-       }
-
-       /**
-        * @covers IP::canonicalize
-        */
-       public function testIPCanonicalizeOnValidIp() {
-               $this->assertEquals( '192.0.2.152', IP::canonicalize( '192.0.2.152' ),
-                       'Canonicalization of a valid IP returns it unchanged' );
-       }
-
-       /**
-        * @covers IP::canonicalize
-        */
-       public function testIPCanonicalizeMappedAddress() {
-               $this->assertEquals(
-                       '192.0.2.152',
-                       IP::canonicalize( '::ffff:192.0.2.152' )
-               );
-               $this->assertEquals(
-                       '192.0.2.152',
-                       IP::canonicalize( '::192.0.2.152' )
-               );
-       }
-
-       /**
-        * Issues there are most probably from IP::toHex() or IP::parseRange()
-        * @covers IP::isInRange
-        * @dataProvider provideIPsAndRanges
-        */
-       public function testIPIsInRange( $expected, $addr, $range, $message = '' ) {
-               $this->assertEquals(
-                       $expected,
-                       IP::isInRange( $addr, $range ),
-                       $message
-               );
-       }
-
-       /** Provider for testIPIsInRange() */
-       public static function provideIPsAndRanges() {
-               # Format: (expected boolean, address, range, optional message)
-               return [
-                       # IPv4
-                       [ true, '192.0.2.0', '192.0.2.0/24', 'Network address' ],
-                       [ true, '192.0.2.77', '192.0.2.0/24', 'Simple address' ],
-                       [ true, '192.0.2.255', '192.0.2.0/24', 'Broadcast address' ],
-
-                       [ false, '0.0.0.0', '192.0.2.0/24' ],
-                       [ false, '255.255.255', '192.0.2.0/24' ],
-
-                       # IPv6
-                       [ false, '::1', '2001:DB8::/32' ],
-                       [ false, '::', '2001:DB8::/32' ],
-                       [ false, 'FE80::1', '2001:DB8::/32' ],
-
-                       [ true, '2001:DB8::', '2001:DB8::/32' ],
-                       [ true, '2001:0DB8::', '2001:DB8::/32' ],
-                       [ true, '2001:DB8::1', '2001:DB8::/32' ],
-                       [ true, '2001:0DB8::1', '2001:DB8::/32' ],
-                       [ true, '2001:0DB8:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF',
-                               '2001:DB8::/32' ],
-
-                       [ false, '2001:0DB8:F::', '2001:DB8::/96' ],
-               ];
-       }
-
-       /**
-        * @covers IP::splitHostAndPort()
-        * @dataProvider provideSplitHostAndPort
-        */
-       public function testSplitHostAndPort( $expected, $input, $description ) {
-               $this->assertEquals( $expected, IP::splitHostAndPort( $input ), $description );
-       }
-
-       /**
-        * Provider for IP::splitHostAndPort()
-        */
-       public static function provideSplitHostAndPort() {
-               return [
-                       [ false, '[', 'Unclosed square bracket' ],
-                       [ false, '[::', 'Unclosed square bracket 2' ],
-                       [ [ '::', false ], '::', 'Bare IPv6 0' ],
-                       [ [ '::1', false ], '::1', 'Bare IPv6 1' ],
-                       [ [ '::', false ], '[::]', 'Bracketed IPv6 0' ],
-                       [ [ '::1', false ], '[::1]', 'Bracketed IPv6 1' ],
-                       [ [ '::1', 80 ], '[::1]:80', 'Bracketed IPv6 with port' ],
-                       [ false, '::x', 'Double colon but no IPv6' ],
-                       [ [ 'x', 80 ], 'x:80', 'Hostname and port' ],
-                       [ false, 'x:x', 'Hostname and invalid port' ],
-                       [ [ 'x', false ], 'x', 'Plain hostname' ]
-               ];
-       }
-
-       /**
-        * @covers IP::combineHostAndPort()
-        * @dataProvider provideCombineHostAndPort
-        */
-       public function testCombineHostAndPort( $expected, $input, $description ) {
-               list( $host, $port, $defaultPort ) = $input;
-               $this->assertEquals(
-                       $expected,
-                       IP::combineHostAndPort( $host, $port, $defaultPort ),
-                       $description );
-       }
-
-       /**
-        * Provider for IP::combineHostAndPort()
-        */
-       public static function provideCombineHostAndPort() {
-               return [
-                       [ '[::1]', [ '::1', 2, 2 ], 'IPv6 default port' ],
-                       [ '[::1]:2', [ '::1', 2, 3 ], 'IPv6 non-default port' ],
-                       [ 'x', [ 'x', 2, 2 ], 'Normal default port' ],
-                       [ 'x:2', [ 'x', 2, 3 ], 'Normal non-default port' ],
-               ];
-       }
-
-       /**
-        * @covers IP::sanitizeRange()
-        * @dataProvider provideIPCIDRs
-        */
-       public function testSanitizeRange( $input, $expected, $description ) {
-               $this->assertEquals( $expected, IP::sanitizeRange( $input ), $description );
-       }
-
-       /**
-        * Provider for IP::testSanitizeRange()
-        */
-       public static function provideIPCIDRs() {
-               return [
-                       [ '35.56.31.252/16', '35.56.0.0/16', 'IPv4 range' ],
-                       [ '135.16.21.252/24', '135.16.21.0/24', 'IPv4 range' ],
-                       [ '5.36.71.252/32', '5.36.71.252/32', 'IPv4 silly range' ],
-                       [ '5.36.71.252', '5.36.71.252', 'IPv4 non-range' ],
-                       [ '0:1:2:3:4:c5:f6:7/96', '0:1:2:3:4:C5:0:0/96', 'IPv6 range' ],
-                       [ '0:1:2:3:4:5:6:7/120', '0:1:2:3:4:5:6:0/120', 'IPv6 range' ],
-                       [ '0:e1:2:3:4:5:e6:7/128', '0:E1:2:3:4:5:E6:7/128', 'IPv6 silly range' ],
-                       [ '0:c1:A2:3:4:5:c6:7', '0:C1:A2:3:4:5:C6:7', 'IPv6 non range' ],
-               ];
-       }
-
-       /**
-        * @covers IP::prettifyIP()
-        * @dataProvider provideIPsToPrettify
-        */
-       public function testPrettifyIP( $ip, $prettified ) {
-               $this->assertEquals( $prettified, IP::prettifyIP( $ip ), "Prettify of $ip" );
-       }
-
-       /**
-        * Provider for IP::testPrettifyIP()
-        */
-       public static function provideIPsToPrettify() {
-               return [
-                       [ '0:0:0:0:0:0:0:0', '::' ],
-                       [ '0:0:0::0:0:0', '::' ],
-                       [ '0:0:0:1:0:0:0:0', '0:0:0:1::' ],
-                       [ '0:0::f', '::f' ],
-                       [ '0::0:0:0:33:fef:b', '::33:fef:b' ],
-                       [ '3f:535:0:0:0:0:e:fbb', '3f:535::e:fbb' ],
-                       [ '0:0:fef:0:0:0:e:fbb', '0:0:fef::e:fbb' ],
-                       [ 'abbc:2004::0:0:0:0', 'abbc:2004::' ],
-                       [ 'cebc:2004:f:0:0:0:0:0', 'cebc:2004:f::' ],
-                       [ '0:0:0:0:0:0:0:0/16', '::/16' ],
-                       [ '0:0:0::0:0:0/64', '::/64' ],
-                       [ '0:0::f/52', '::f/52' ],
-                       [ '::0:0:33:fef:b/52', '::33:fef:b/52' ],
-                       [ '3f:535:0:0:0:0:e:fbb/48', '3f:535::e:fbb/48' ],
-                       [ '0:0:fef:0:0:0:e:fbb/96', '0:0:fef::e:fbb/96' ],
-                       [ 'abbc:2004:0:0::0:0/40', 'abbc:2004::/40' ],
-                       [ 'aebc:2004:f:0:0:0:0:0/80', 'aebc:2004:f::/80' ],
-               ];
-       }
-}
diff --git a/tests/phpunit/includes/libs/JavaScriptMinifierTest.php b/tests/phpunit/includes/libs/JavaScriptMinifierTest.php
deleted file mode 100644 (file)
index d57d0dd..0000000
+++ /dev/null
@@ -1,367 +0,0 @@
-<?php
-
-class JavaScriptMinifierTest extends PHPUnit\Framework\TestCase {
-
-       use MediaWikiCoversValidator;
-
-       protected function tearDown() {
-               parent::tearDown();
-               // Reset
-               $this->setMaxLineLength( 1000 );
-       }
-
-       private function setMaxLineLength( $val ) {
-               $classReflect = new ReflectionClass( JavaScriptMinifier::class );
-               $propertyReflect = $classReflect->getProperty( 'maxLineLength' );
-               $propertyReflect->setAccessible( true );
-               $propertyReflect->setValue( JavaScriptMinifier::class, $val );
-       }
-
-       public static function provideCases() {
-               return [
-
-                       // Basic whitespace and comments that should be stripped entirely
-                       [ "\r\t\f \v\n\r", "" ],
-                       [ "/* Foo *\n*bar\n*/", "" ],
-
-                       /**
-                        * Slashes used inside block comments (T28931).
-                        * At some point there was a bug that caused this comment to be ended at '* /',
-                        * causing /M... to be left as the beginning of a regex.
-                        */
-                       [
-                               "/**\n * Foo\n * {\n * 'bar' : {\n * "
-                                       . "//Multiple rules with configurable operators\n * 'baz' : false\n * }\n */",
-                               "" ],
-
-                       /**
-                        * '  Foo \' bar \
-                        *  baz \' quox '  .
-                        */
-                       [
-                               "'  Foo  \\'  bar  \\\n  baz  \\'  quox  '  .length",
-                               "'  Foo  \\'  bar  \\\n  baz  \\'  quox  '.length"
-                       ],
-                       [
-                               "\"  Foo  \\\"  bar  \\\n  baz  \\\"  quox  \"  .length",
-                               "\"  Foo  \\\"  bar  \\\n  baz  \\\"  quox  \".length"
-                       ],
-                       [ "// Foo b/ar baz", "" ],
-                       [
-                               "/  Foo  \\/  bar  [  /  \\]  /  ]  baz  /  .length",
-                               "/  Foo  \\/  bar  [  /  \\]  /  ]  baz  /.length"
-                       ],
-
-                       // HTML comments
-                       [ "<!-- Foo bar", "" ],
-                       [ "<!-- Foo --> bar", "" ],
-                       [ "--> Foo", "" ],
-                       [ "x --> y", "x-->y" ],
-
-                       // Semicolon insertion
-                       [ "(function(){return\nx;})", "(function(){return\nx;})" ],
-                       [ "throw\nx;", "throw\nx;" ],
-                       [ "while(p){continue\nx;}", "while(p){continue\nx;}" ],
-                       [ "while(p){break\nx;}", "while(p){break\nx;}" ],
-                       [ "var\nx;", "var x;" ],
-                       [ "x\ny;", "x\ny;" ],
-                       [ "x\n++y;", "x\n++y;" ],
-                       [ "x\n!y;", "x\n!y;" ],
-                       [ "x\n{y}", "x\n{y}" ],
-                       [ "x\n+y;", "x+y;" ],
-                       [ "x\n(y);", "x(y);" ],
-                       [ "5.\nx;", "5.\nx;" ],
-                       [ "0xFF.\nx;", "0xFF.x;" ],
-                       [ "5.3.\nx;", "5.3.x;" ],
-
-                       // Cover failure case for incomplete hex literal
-                       [ "0x;", false, false ],
-
-                       // Cover failure case for number with no digits after E
-                       [ "1.4E", false, false ],
-
-                       // Cover failure case for number with several E
-                       [ "1.4EE2", false, false ],
-                       [ "1.4EE", false, false ],
-
-                       // Cover failure case for number with several E (nonconsecutive)
-                       // FIXME: This is invalid, but currently tolerated
-                       [ "1.4E2E3", "1.4E2 E3", false ],
-
-                       // Semicolon insertion between an expression having an inline
-                       // comment after it, and a statement on the next line (T29046).
-                       [
-                               "var a = this //foo bar \n for ( b = 0; c < d; b++ ) {}",
-                               "var a=this\nfor(b=0;c<d;b++){}"
-                       ],
-
-                       // Cover failure case of incomplete regexp at end of file (T75556)
-                       // FIXME: This is invalid, but currently tolerated
-                       [ "*/", "*/", false ],
-
-                       // Cover failure case of incomplete char class in regexp (T75556)
-                       // FIXME: This is invalid, but currently tolerated
-                       [ "/a[b/.test", "/a[b/.test", false ],
-
-                       // Cover failure case of incomplete string at end of file (T75556)
-                       // FIXME: This is invalid, but currently tolerated
-                       [ "'a", "'a", false ],
-
-                       // Token separation
-                       [ "x  in  y", "x in y" ],
-                       [ "/x/g  in  y", "/x/g in y" ],
-                       [ "x  in  30", "x in 30" ],
-                       [ "x  +  ++  y", "x+ ++y" ],
-                       [ "x ++  +  y", "x++ +y" ],
-                       [ "x  /  /y/.exec(z)", "x/ /y/.exec(z)" ],
-
-                       // State machine
-                       [ "/  x/g", "/  x/g" ],
-                       [ "(function(){return/  x/g})", "(function(){return/  x/g})" ],
-                       [ "+/  x/g", "+/  x/g" ],
-                       [ "++/  x/g", "++/  x/g" ],
-                       [ "x/  x/g", "x/x/g" ],
-                       [ "(/  x/g)", "(/  x/g)" ],
-                       [ "if(/  x/g);", "if(/  x/g);" ],
-                       [ "(x/  x/g)", "(x/x/g)" ],
-                       [ "([/  x/g])", "([/  x/g])" ],
-                       [ "+x/  x/g", "+x/x/g" ],
-                       [ "{}/  x/g", "{}/  x/g" ],
-                       [ "+{}/  x/g", "+{}/x/g" ],
-                       [ "(x)/  x/g", "(x)/x/g" ],
-                       [ "if(x)/  x/g", "if(x)/  x/g" ],
-                       [ "for(x;x;{}/  x/g);", "for(x;x;{}/x/g);" ],
-                       [ "x;x;{}/  x/g", "x;x;{}/  x/g" ],
-                       [ "x:{}/  x/g", "x:{}/  x/g" ],
-                       [ "switch(x){case y?z:{}/  x/g:{}/  x/g;}", "switch(x){case y?z:{}/x/g:{}/  x/g;}" ],
-                       [ "function x(){}/  x/g", "function x(){}/  x/g" ],
-                       [ "+function x(){}/  x/g", "+function x(){}/x/g" ],
-
-                       // Multiline quoted string
-                       [ "var foo=\"\\\nblah\\\n\";", "var foo=\"\\\nblah\\\n\";" ],
-
-                       // Multiline quoted string followed by string with spaces
-                       [
-                               "var foo=\"\\\nblah\\\n\";\nvar baz = \" foo \";\n",
-                               "var foo=\"\\\nblah\\\n\";var baz=\" foo \";"
-                       ],
-
-                       // URL in quoted string ( // is not a comment)
-                       [
-                               "aNode.setAttribute('href','http://foo.bar.org/baz');",
-                               "aNode.setAttribute('href','http://foo.bar.org/baz');"
-                       ],
-
-                       // URL in quoted string after multiline quoted string
-                       [
-                               "var foo=\"\\\nblah\\\n\";\naNode.setAttribute('href','http://foo.bar.org/baz');",
-                               "var foo=\"\\\nblah\\\n\";aNode.setAttribute('href','http://foo.bar.org/baz');"
-                       ],
-
-                       // Division vs. regex nastiness
-                       [
-                               "alert( (10+10) / '/'.charCodeAt( 0 ) + '//' );",
-                               "alert((10+10)/'/'.charCodeAt(0)+'//');"
-                       ],
-                       [ "if(1)/a /g.exec('Pa ss');", "if(1)/a /g.exec('Pa ss');" ],
-
-                       // Unicode letter characters should pass through ok in identifiers (T33187)
-                       [ "var KaŝSkatolVal = {}", 'var KaŝSkatolVal={}' ],
-
-                       // Per spec unicode char escape values should work in identifiers,
-                       // as long as it's a valid char. In future it might get normalized.
-                       [ "var Ka\\u015dSkatolVal = {}", 'var Ka\\u015dSkatolVal={}' ],
-
-                       // Some structures that might look invalid at first sight
-                       [ "var a = 5.;", "var a=5.;" ],
-                       [ "5.0.toString();", "5.0.toString();" ],
-                       [ "5..toString();", "5..toString();" ],
-                       // Cover failure case for too many decimal points
-                       [ "5...toString();", false ],
-                       [ "5.\n.toString();", '5..toString();' ],
-
-                       // Boolean minification (!0 / !1)
-                       [ "var a = { b: true };", "var a={b:!0};" ],
-                       [ "var a = { true: 12 };", "var a={true:12};" ],
-                       [ "a.true = 12;", "a.true=12;" ],
-                       [ "a.foo = true;", "a.foo=!0;" ],
-                       [ "a.foo = false;", "a.foo=!1;" ],
-               ];
-       }
-
-       /**
-        * @dataProvider provideCases
-        * @covers JavaScriptMinifier::minify
-        * @covers JavaScriptMinifier::parseError
-        */
-       public function testMinifyOutput( $code, $expectedOutput, $expectedValid = true ) {
-               $minified = JavaScriptMinifier::minify( $code );
-
-               // JSMin+'s parser will throw an exception if output is not valid JS.
-               // suppression of warnings needed for stupid crap
-               if ( $expectedValid ) {
-                       Wikimedia\suppressWarnings();
-                       $parser = new JSParser();
-                       Wikimedia\restoreWarnings();
-                       $parser->parse( $minified, 'minify-test.js', 1 );
-               }
-
-               $this->assertEquals(
-                       $expectedOutput,
-                       $minified,
-                       "Minified output should be in the form expected."
-               );
-       }
-
-       public static function provideLineBreaker() {
-               return [
-                       [
-                               // Regression tests for T34548.
-                               // Must not break between 'E' and '+'.
-                               'var name = 1.23456789E55;',
-                               [
-                                       'var',
-                                       'name',
-                                       '=',
-                                       '1.23456789E55',
-                                       ';',
-                               ],
-                       ],
-                       [
-                               'var name = 1.23456789E+5;',
-                               [
-                                       'var',
-                                       'name',
-                                       '=',
-                                       '1.23456789E+5',
-                                       ';',
-                               ],
-                       ],
-                       [
-                               'var name = 1.23456789E-5;',
-                               [
-                                       'var',
-                                       'name',
-                                       '=',
-                                       '1.23456789E-5',
-                                       ';',
-                               ],
-                       ],
-                       [
-                               // Must not break before '++'
-                               'if(x++);',
-                               [
-                                       'if',
-                                       '(',
-                                       'x++',
-                                       ')',
-                                       ';',
-                               ],
-                       ],
-                       [
-                               // Regression test for T201606.
-                               // Must not break between 'return' and Expression.
-                               // Was caused by bad state after '{}' in property value.
-                               <<<JAVASCRIPT
-                       call( function () {
-                               try {
-                               } catch (e) {
-                                       obj = {
-                                               key: 1 ? 0 : {}
-                                       };
-                               }
-                               return name === 'input';
-                       } );
-JAVASCRIPT
-                               ,
-                               [
-                                       'call',
-                                       '(',
-                                       'function',
-                                       '(',
-                                       ')',
-                                       '{',
-                                       'try',
-                                       '{',
-                                       '}',
-                                       'catch',
-                                       '(',
-                                       'e',
-                                       ')',
-                                       '{',
-                                       'obj',
-                                       '=',
-                                       '{',
-                                       'key',
-                                       ':',
-                                       '1',
-                                       '?',
-                                       '0',
-                                       ':',
-                                       '{',
-                                       '}',
-                                       '}',
-                                       ';',
-                                       '}',
-                                       // The return Statement:
-                                       //     return [no LineTerminator here] Expression
-                                       'return name',
-                                       '===',
-                                       "'input'",
-                                       ';',
-                                       '}',
-                                       ')',
-                                       ';',
-                               ]
-                       ],
-                       [
-                               // Regression test for T201606.
-                               // Must not break between 'return' and Expression.
-                               // Was caused by bad state after a ternary in the expression value
-                               // for a key in an object literal.
-                               <<<JAVASCRIPT
-call( {
-       key: 1 ? 0 : function () {
-               return this;
-       }
-} );
-JAVASCRIPT
-                               ,
-                               [
-                                       'call',
-                                       '(',
-                                       '{',
-                                       'key',
-                                       ':',
-                                       '1',
-                                       '?',
-                                       '0',
-                                       ':',
-                                       'function',
-                                       '(',
-                                       ')',
-                                       '{',
-                                       'return this',
-                                       ';',
-                                       '}',
-                                       '}',
-                                       ')',
-                                       ';',
-                               ]
-                       ],
-               ];
-       }
-
-       /**
-        * @dataProvider provideLineBreaker
-        * @covers JavaScriptMinifier::minify
-        */
-       public function testLineBreaker( $code, array $expectedLines ) {
-               $this->setMaxLineLength( 1 );
-               $actual = JavaScriptMinifier::minify( $code );
-               $this->assertEquals(
-                       array_merge( [ '' ], $expectedLines ),
-                       explode( "\n", $actual )
-               );
-       }
-}
diff --git a/tests/phpunit/includes/libs/MapCacheLRUTest.php b/tests/phpunit/includes/libs/MapCacheLRUTest.php
deleted file mode 100644 (file)
index 7147c6f..0000000
+++ /dev/null
@@ -1,267 +0,0 @@
-<?php
-/**
- * @group Cache
- */
-class MapCacheLRUTest extends PHPUnit\Framework\TestCase {
-
-       use MediaWikiCoversValidator;
-
-       /**
-        * @covers MapCacheLRU::newFromArray()
-        * @covers MapCacheLRU::toArray()
-        * @covers MapCacheLRU::getAllKeys()
-        * @covers MapCacheLRU::clear()
-        * @covers MapCacheLRU::getMaxSize()
-        * @covers MapCacheLRU::setMaxSize()
-        */
-       function testArrayConversion() {
-               $raw = [ 'd' => 4, 'c' => 3, 'b' => 2, 'a' => 1 ];
-               $cache = MapCacheLRU::newFromArray( $raw, 3 );
-
-               $this->assertEquals( 3, $cache->getMaxSize() );
-               $this->assertSame( true, $cache->has( 'a' ) );
-               $this->assertSame( true, $cache->has( 'b' ) );
-               $this->assertSame( true, $cache->has( 'c' ) );
-               $this->assertSame( 1, $cache->get( 'a' ) );
-               $this->assertSame( 2, $cache->get( 'b' ) );
-               $this->assertSame( 3, $cache->get( 'c' ) );
-
-               $this->assertSame(
-                       [ 'a' => 1, 'b' => 2, 'c' => 3 ],
-                       $cache->toArray()
-               );
-               $this->assertSame(
-                       [ 'a', 'b', 'c' ],
-                       $cache->getAllKeys()
-               );
-
-               $cache->clear( 'a' );
-               $this->assertSame(
-                       [ 'b' => 2, 'c' => 3 ],
-                       $cache->toArray()
-               );
-
-               $cache->clear();
-               $this->assertSame(
-                       [],
-                       $cache->toArray()
-               );
-
-               $cache = MapCacheLRU::newFromArray( [ 'd' => 4, 'c' => 3, 'b' => 2, 'a' => 1 ], 4 );
-               $cache->setMaxSize( 3 );
-               $this->assertSame(
-                       [ 'c' => 3, 'b' => 2, 'a' => 1 ],
-                       $cache->toArray()
-               );
-       }
-
-       /**
-        * @covers MapCacheLRU::serialize()
-        * @covers MapCacheLRU::unserialize()
-        */
-       function testSerialize() {
-               $cache = MapCacheLRU::newFromArray( [ 'd' => 4, 'c' => 3, 'b' => 2, 'a' => 1 ], 10 );
-               $string = serialize( $cache );
-               $ncache = unserialize( $string );
-               $this->assertSame(
-                       [ 'd' => 4, 'c' => 3, 'b' => 2, 'a' => 1 ],
-                       $ncache->toArray()
-               );
-       }
-
-       /**
-        * @covers MapCacheLRU::has()
-        * @covers MapCacheLRU::get()
-        * @covers MapCacheLRU::set()
-        */
-       function testLRU() {
-               $raw = [ 'a' => 1, 'b' => 2, 'c' => 3 ];
-               $cache = MapCacheLRU::newFromArray( $raw, 3 );
-
-               $this->assertSame( true, $cache->has( 'c' ) );
-               $this->assertSame(
-                       [ 'a' => 1, 'b' => 2, 'c' => 3 ],
-                       $cache->toArray()
-               );
-
-               $this->assertSame( 3, $cache->get( 'c' ) );
-               $this->assertSame(
-                       [ 'a' => 1, 'b' => 2, 'c' => 3 ],
-                       $cache->toArray()
-               );
-
-               $this->assertSame( 1, $cache->get( 'a' ) );
-               $this->assertSame(
-                       [ 'b' => 2, 'c' => 3, 'a' => 1 ],
-                       $cache->toArray()
-               );
-
-               $cache->set( 'a', 1 );
-               $this->assertSame(
-                       [ 'b' => 2, 'c' => 3, 'a' => 1 ],
-                       $cache->toArray()
-               );
-
-               $cache->set( 'b', 22 );
-               $this->assertSame(
-                       [ 'c' => 3, 'a' => 1, 'b' => 22 ],
-                       $cache->toArray()
-               );
-
-               $cache->set( 'd', 4 );
-               $this->assertSame(
-                       [ 'a' => 1, 'b' => 22, 'd' => 4 ],
-                       $cache->toArray()
-               );
-
-               $cache->set( 'e', 5, 0.33 );
-               $this->assertSame(
-                       [ 'e' => 5, 'b' => 22, 'd' => 4 ],
-                       $cache->toArray()
-               );
-
-               $cache->set( 'f', 6, 0.66 );
-               $this->assertSame(
-                       [ 'b' => 22, 'f' => 6, 'd' => 4 ],
-                       $cache->toArray()
-               );
-
-               $cache->set( 'g', 7, 0.90 );
-               $this->assertSame(
-                       [ 'f' => 6, 'g' => 7, 'd' => 4 ],
-                       $cache->toArray()
-               );
-
-               $cache->set( 'g', 7, 1.0 );
-               $this->assertSame(
-                       [ 'f' => 6, 'd' => 4, 'g' => 7 ],
-                       $cache->toArray()
-               );
-       }
-
-       /**
-        * @covers MapCacheLRU::has()
-        * @covers MapCacheLRU::get()
-        * @covers MapCacheLRU::set()
-        */
-       public function testExpiry() {
-               $raw = [ 'a' => 1, 'b' => 2, 'c' => 3 ];
-               $cache = MapCacheLRU::newFromArray( $raw, 3 );
-
-               $now = microtime( true );
-               $cache->setMockTime( $now );
-
-               $cache->set( 'd', 'xxx' );
-               $this->assertTrue( $cache->has( 'd', 30 ) );
-               $this->assertEquals( 'xxx', $cache->get( 'd' ) );
-
-               $now += 29;
-               $this->assertTrue( $cache->has( 'd', 30 ) );
-               $this->assertEquals( 'xxx', $cache->get( 'd' ) );
-               $this->assertEquals( 'xxx', $cache->get( 'd', 30 ) );
-
-               $now += 1.5;
-               $this->assertFalse( $cache->has( 'd', 30 ) );
-               $this->assertEquals( 'xxx', $cache->get( 'd' ) );
-               $this->assertNull( $cache->get( 'd', 30 ) );
-       }
-
-       /**
-        * @covers MapCacheLRU::hasField()
-        * @covers MapCacheLRU::getField()
-        * @covers MapCacheLRU::setField()
-        */
-       public function testFields() {
-               $raw = [ 'a' => 1, 'b' => 2, 'c' => 3 ];
-               $cache = MapCacheLRU::newFromArray( $raw, 3 );
-
-               $now = microtime( true );
-               $cache->setMockTime( $now );
-
-               $cache->setField( 'PMs', 'Tony Blair', 'Labour' );
-               $cache->setField( 'PMs', 'Margaret Thatcher', 'Tory' );
-               $this->assertTrue( $cache->hasField( 'PMs', 'Tony Blair', 30 ) );
-               $this->assertEquals( 'Labour', $cache->getField( 'PMs', 'Tony Blair' ) );
-               $this->assertTrue( $cache->hasField( 'PMs', 'Tony Blair', 30 ) );
-
-               $now += 29;
-               $this->assertTrue( $cache->hasField( 'PMs', 'Tony Blair', 30 ) );
-               $this->assertEquals( 'Labour', $cache->getField( 'PMs', 'Tony Blair' ) );
-               $this->assertEquals( 'Labour', $cache->getField( 'PMs', 'Tony Blair', 30 ) );
-
-               $now += 1.5;
-               $this->assertFalse( $cache->hasField( 'PMs', 'Tony Blair', 30 ) );
-               $this->assertEquals( 'Labour', $cache->getField( 'PMs', 'Tony Blair' ) );
-               $this->assertNull( $cache->getField( 'PMs', 'Tony Blair', 30 ) );
-
-               $this->assertEquals(
-                       [ 'Tony Blair' => 'Labour', 'Margaret Thatcher' => 'Tory' ],
-                       $cache->get( 'PMs' )
-               );
-
-               $cache->set( 'MPs', [
-                       'Edwina Currie' => 1983,
-                       'Neil Kinnock' => 1970
-               ] );
-               $this->assertEquals(
-                       [
-                               'Edwina Currie' => 1983,
-                               'Neil Kinnock' => 1970
-                       ],
-                       $cache->get( 'MPs' )
-               );
-
-               $this->assertEquals( 1983, $cache->getField( 'MPs', 'Edwina Currie' ) );
-               $this->assertEquals( 1970, $cache->getField( 'MPs', 'Neil Kinnock' ) );
-       }
-
-       /**
-        * @covers MapCacheLRU::has()
-        * @covers MapCacheLRU::get()
-        * @covers MapCacheLRU::set()
-        * @covers MapCacheLRU::hasField()
-        * @covers MapCacheLRU::getField()
-        * @covers MapCacheLRU::setField()
-        */
-       public function testInvalidKeys() {
-               $cache = MapCacheLRU::newFromArray( [], 3 );
-
-               try {
-                       $cache->has( 3.4 );
-                       $this->fail( "No exception" );
-               } catch ( UnexpectedValueException $e ) {
-                       $this->assertRegExp( '/must be string or integer/', $e->getMessage() );
-               }
-               try {
-                       $cache->get( false );
-                       $this->fail( "No exception" );
-               } catch ( UnexpectedValueException $e ) {
-                       $this->assertRegExp( '/must be string or integer/', $e->getMessage() );
-               }
-               try {
-                       $cache->set( 3.4, 'x' );
-                       $this->fail( "No exception" );
-               } catch ( UnexpectedValueException $e ) {
-                       $this->assertRegExp( '/must be string or integer/', $e->getMessage() );
-               }
-
-               try {
-                       $cache->hasField( 'x', 3.4 );
-                       $this->fail( "No exception" );
-               } catch ( UnexpectedValueException $e ) {
-                       $this->assertRegExp( '/must be string or integer/', $e->getMessage() );
-               }
-               try {
-                       $cache->getField( 'x', false );
-                       $this->fail( "No exception" );
-               } catch ( UnexpectedValueException $e ) {
-                       $this->assertRegExp( '/must be string or integer/', $e->getMessage() );
-               }
-               try {
-                       $cache->setField( 'x', 3.4, 'x' );
-                       $this->fail( "No exception" );
-               } catch ( UnexpectedValueException $e ) {
-                       $this->assertRegExp( '/must be string or integer/', $e->getMessage() );
-               }
-       }
-}
diff --git a/tests/phpunit/includes/libs/MemoizedCallableTest.php b/tests/phpunit/includes/libs/MemoizedCallableTest.php
deleted file mode 100644 (file)
index 628cca0..0000000
+++ /dev/null
@@ -1,142 +0,0 @@
-<?php
-/**
- * PHPUnit tests for MemoizedCallable class.
- * @covers MemoizedCallable
- */
-class MemoizedCallableTest extends PHPUnit\Framework\TestCase {
-
-       use MediaWikiCoversValidator;
-
-       /**
-        * The memoized callable should relate inputs to outputs in the same
-        * way as the original underlying callable.
-        */
-       public function testReturnValuePassedThrough() {
-               $mock = $this->getMockBuilder( stdClass::class )
-                       ->setMethods( [ 'reverse' ] )->getMock();
-               $mock->expects( $this->any() )
-                       ->method( 'reverse' )
-                       ->will( $this->returnCallback( 'strrev' ) );
-
-               $memoized = new MemoizedCallable( [ $mock, 'reverse' ] );
-               $this->assertEquals( 'flow', $memoized->invoke( 'wolf' ) );
-       }
-
-       /**
-        * Consecutive calls to the memoized callable with the same arguments
-        * should result in just one invocation of the underlying callable.
-        *
-        * @requires extension apcu
-        */
-       public function testCallableMemoized() {
-               $observer = $this->getMockBuilder( stdClass::class )
-                       ->setMethods( [ 'computeSomething' ] )->getMock();
-               $observer->expects( $this->once() )
-                       ->method( 'computeSomething' )
-                       ->will( $this->returnValue( 'ok' ) );
-
-               $memoized = new ArrayBackedMemoizedCallable( [ $observer, 'computeSomething' ] );
-
-               // First invocation -- delegates to $observer->computeSomething()
-               $this->assertEquals( 'ok', $memoized->invoke() );
-
-               // Second invocation -- returns memoized result
-               $this->assertEquals( 'ok', $memoized->invoke() );
-       }
-
-       /**
-        * @covers MemoizedCallable::invoke
-        */
-       public function testInvokeVariadic() {
-               $memoized = new MemoizedCallable( 'sprintf' );
-               $this->assertEquals(
-                       $memoized->invokeArgs( [ 'this is %s', 'correct' ] ),
-                       $memoized->invoke( 'this is %s', 'correct' )
-               );
-       }
-
-       /**
-        * @covers MemoizedCallable::call
-        */
-       public function testShortcutMethod() {
-               $this->assertEquals(
-                       'this is correct',
-                       MemoizedCallable::call( 'sprintf', [ 'this is %s', 'correct' ] )
-               );
-       }
-
-       /**
-        * Outlier TTL values should be coerced to range 1 - 86400.
-        */
-       public function testTTLMaxMin() {
-               $memoized = new MemoizedCallable( 'abs', 100000 );
-               $this->assertEquals( 86400, $this->readAttribute( $memoized, 'ttl' ) );
-
-               $memoized = new MemoizedCallable( 'abs', -10 );
-               $this->assertEquals( 1, $this->readAttribute( $memoized, 'ttl' ) );
-       }
-
-       /**
-        * Closure names should be distinct.
-        */
-       public function testMemoizedClosure() {
-               $a = new MemoizedCallable( function () {
-                       return 'a';
-               } );
-
-               $b = new MemoizedCallable( function () {
-                       return 'b';
-               } );
-
-               $this->assertEquals( $a->invokeArgs(), 'a' );
-               $this->assertEquals( $b->invokeArgs(), 'b' );
-
-               $this->assertNotEquals(
-                       $this->readAttribute( $a, 'callableName' ),
-                       $this->readAttribute( $b, 'callableName' )
-               );
-
-               $c = new ArrayBackedMemoizedCallable( function () {
-                       return rand();
-               } );
-               $this->assertEquals( $c->invokeArgs(), $c->invokeArgs(), 'memoized random' );
-       }
-
-       /**
-        * @expectedExceptionMessage non-scalar argument
-        * @expectedException        InvalidArgumentException
-        */
-       public function testNonScalarArguments() {
-               $memoized = new MemoizedCallable( 'gettype' );
-               $memoized->invoke( new stdClass() );
-       }
-
-       /**
-        * @expectedExceptionMessage must be an instance of callable
-        * @expectedException        InvalidArgumentException
-        */
-       public function testNotCallable() {
-               $memoized = new MemoizedCallable( 14 );
-       }
-}
-
-/**
- * A MemoizedCallable subclass that stores function return values
- * in an instance property rather than APC or APCu.
- */
-class ArrayBackedMemoizedCallable extends MemoizedCallable {
-       private $cache = [];
-
-       protected function fetchResult( $key, &$success ) {
-               if ( array_key_exists( $key, $this->cache ) ) {
-                       $success = true;
-                       return $this->cache[$key];
-               }
-               $success = false;
-               return false;
-       }
-
-       protected function storeResult( $key, $result ) {
-               $this->cache[$key] = $result;
-       }
-}
diff --git a/tests/phpunit/includes/libs/ProcessCacheLRUTest.php b/tests/phpunit/includes/libs/ProcessCacheLRUTest.php
deleted file mode 100644 (file)
index 8e91e70..0000000
+++ /dev/null
@@ -1,264 +0,0 @@
-<?php
-
-/**
- * Note that it uses the ProcessCacheLRUTestable class which extends some
- * properties and methods visibility. That class is defined at the end of the
- * file containing this class.
- *
- * @group Cache
- */
-class ProcessCacheLRUTest extends PHPUnit\Framework\TestCase {
-
-       use MediaWikiCoversValidator;
-
-       /**
-        * Helper to verify emptiness of a cache object.
-        * Compare against an array so we get the cache content difference.
-        */
-       protected function assertCacheEmpty( $cache, $msg = 'Cache should be empty' ) {
-               $this->assertEquals( 0, $cache->getEntriesCount(), $msg );
-       }
-
-       /**
-        * Helper to fill a cache object passed by reference
-        */
-       protected function fillCache( &$cache, $numEntries ) {
-               // Fill cache with three values
-               for ( $i = 1; $i <= $numEntries; $i++ ) {
-                       $cache->set( "cache-key-$i", "prop-$i", "value-$i" );
-               }
-       }
-
-       /**
-        * Generates an array of what would be expected in cache for a given cache
-        * size and a number of entries filled in sequentially
-        */
-       protected function getExpectedCache( $cacheMaxEntries, $entryToFill ) {
-               $expected = [];
-
-               if ( $entryToFill === 0 ) {
-                       // The cache is empty!
-                       return [];
-               } elseif ( $entryToFill <= $cacheMaxEntries ) {
-                       // Cache is not fully filled
-                       $firstKey = 1;
-               } else {
-                       // Cache overflowed
-                       $firstKey = 1 + $entryToFill - $cacheMaxEntries;
-               }
-
-               $lastKey = $entryToFill;
-
-               for ( $i = $firstKey; $i <= $lastKey; $i++ ) {
-                       $expected["cache-key-$i"] = [ "prop-$i" => "value-$i" ];
-               }
-
-               return $expected;
-       }
-
-       /**
-        * Highlight diff between assertEquals and assertNotSame
-        * @coversNothing
-        */
-       public function testPhpUnitArrayEquality() {
-               $one = [ 'A' => 1, 'B' => 2 ];
-               $two = [ 'B' => 2, 'A' => 1 ];
-               // ==
-               $this->assertEquals( $one, $two );
-               // ===
-               $this->assertNotSame( $one, $two );
-       }
-
-       /**
-        * @dataProvider provideInvalidConstructorArg
-        * @expectedException Wikimedia\Assert\ParameterAssertionException
-        * @covers ProcessCacheLRU::__construct
-        */
-       public function testConstructorGivenInvalidValue( $maxSize ) {
-               new ProcessCacheLRUTestable( $maxSize );
-       }
-
-       /**
-        * Value which are forbidden by the constructor
-        */
-       public static function provideInvalidConstructorArg() {
-               return [
-                       [ null ],
-                       [ [] ],
-                       [ new stdClass() ],
-                       [ 0 ],
-                       [ '5' ],
-                       [ -1 ],
-               ];
-       }
-
-       /**
-        * @covers ProcessCacheLRU::get
-        * @covers ProcessCacheLRU::set
-        * @covers ProcessCacheLRU::has
-        */
-       public function testAddAndGetAKey() {
-               $oneCache = new ProcessCacheLRUTestable( 1 );
-               $this->assertCacheEmpty( $oneCache );
-
-               // First set just one value
-               $oneCache->set( 'cache-key', 'prop1', 'value1' );
-               $this->assertEquals( 1, $oneCache->getEntriesCount() );
-               $this->assertTrue( $oneCache->has( 'cache-key', 'prop1' ) );
-               $this->assertEquals( 'value1', $oneCache->get( 'cache-key', 'prop1' ) );
-       }
-
-       /**
-        * @covers ProcessCacheLRU::set
-        * @covers ProcessCacheLRU::get
-        */
-       public function testDeleteOldKey() {
-               $oneCache = new ProcessCacheLRUTestable( 1 );
-               $this->assertCacheEmpty( $oneCache );
-
-               $oneCache->set( 'cache-key', 'prop1', 'value1' );
-               $oneCache->set( 'cache-key', 'prop1', 'value2' );
-               $this->assertEquals( 'value2', $oneCache->get( 'cache-key', 'prop1' ) );
-       }
-
-       /**
-        * This test that we properly overflow when filling a cache with
-        * a sequence of always different cache-keys. Meant to verify we correclty
-        * delete the older key.
-        *
-        * @covers ProcessCacheLRU::set
-        * @dataProvider provideCacheFilling
-        * @param int $cacheMaxEntries Maximum entry the created cache will hold
-        * @param int $entryToFill Number of entries to insert in the created cache.
-        */
-       public function testFillingCache( $cacheMaxEntries, $entryToFill, $msg = '' ) {
-               $cache = new ProcessCacheLRUTestable( $cacheMaxEntries );
-               $this->fillCache( $cache, $entryToFill );
-
-               $this->assertSame(
-                       $this->getExpectedCache( $cacheMaxEntries, $entryToFill ),
-                       $cache->getCache(),
-                       "Filling a $cacheMaxEntries entries cache with $entryToFill entries"
-               );
-       }
-
-       /**
-        * Provider for testFillingCache
-        */
-       public static function provideCacheFilling() {
-               // ($cacheMaxEntries, $entryToFill, $msg='')
-               return [
-                       [ 1, 0 ],
-                       [ 1, 1 ],
-                       // overflow
-                       [ 1, 2 ],
-                       // overflow
-                       [ 5, 33 ],
-               ];
-       }
-
-       /**
-        * Create a cache with only one remaining entry then update
-        * the first inserted entry. Should bump it to the top.
-        *
-        * @covers ProcessCacheLRU::set
-        */
-       public function testReplaceExistingKeyShouldBumpEntryToTop() {
-               $maxEntries = 3;
-
-               $cache = new ProcessCacheLRUTestable( $maxEntries );
-               // Fill cache leaving just one remaining slot
-               $this->fillCache( $cache, $maxEntries - 1 );
-
-               // Set an existing cache key
-               $cache->set( "cache-key-1", "prop-1", "new-value-for-1" );
-
-               $this->assertSame(
-                       [
-                               'cache-key-2' => [ 'prop-2' => 'value-2' ],
-                               'cache-key-1' => [ 'prop-1' => 'new-value-for-1' ],
-                       ],
-                       $cache->getCache()
-               );
-       }
-
-       /**
-        * @covers ProcessCacheLRU::get
-        * @covers ProcessCacheLRU::set
-        * @covers ProcessCacheLRU::has
-        */
-       public function testRecentlyAccessedKeyStickIn() {
-               $cache = new ProcessCacheLRUTestable( 2 );
-               $cache->set( 'first', 'prop1', 'value1' );
-               $cache->set( 'second', 'prop2', 'value2' );
-
-               // Get first
-               $cache->get( 'first', 'prop1' );
-               // Cache a third value, should invalidate the least used one
-               $cache->set( 'third', 'prop3', 'value3' );
-
-               $this->assertFalse( $cache->has( 'second', 'prop2' ) );
-       }
-
-       /**
-        * This first create a full cache then update the value for the 2nd
-        * filled entry.
-        * Given a cache having 1,2,3 as key, updating 2 should bump 2 to
-        * the top of the queue with the new value: 1,3,2* (* = updated).
-        *
-        * @covers ProcessCacheLRU::set
-        * @covers ProcessCacheLRU::get
-        */
-       public function testReplaceExistingKeyInAFullCacheShouldBumpToTop() {
-               $maxEntries = 3;
-
-               $cache = new ProcessCacheLRUTestable( $maxEntries );
-               $this->fillCache( $cache, $maxEntries );
-
-               // Set an existing cache key
-               $cache->set( "cache-key-2", "prop-2", "new-value-for-2" );
-               $this->assertSame(
-                       [
-                               'cache-key-1' => [ 'prop-1' => 'value-1' ],
-                               'cache-key-3' => [ 'prop-3' => 'value-3' ],
-                               'cache-key-2' => [ 'prop-2' => 'new-value-for-2' ],
-                       ],
-                       $cache->getCache()
-               );
-               $this->assertEquals( 'new-value-for-2',
-                       $cache->get( 'cache-key-2', 'prop-2' )
-               );
-       }
-
-       /**
-        * @covers ProcessCacheLRU::set
-        */
-       public function testBumpExistingKeyToTop() {
-               $cache = new ProcessCacheLRUTestable( 3 );
-               $this->fillCache( $cache, 3 );
-
-               // Set the very first cache key to a new value
-               $cache->set( "cache-key-1", "prop-1", "new value for 1" );
-               $this->assertEquals(
-                       [
-                               'cache-key-2' => [ 'prop-2' => 'value-2' ],
-                               'cache-key-3' => [ 'prop-3' => 'value-3' ],
-                               'cache-key-1' => [ 'prop-1' => 'new value for 1' ],
-                       ],
-                       $cache->getCache()
-               );
-       }
-}
-
-/**
- * Overrides some ProcessCacheLRU methods and properties accessibility.
- */
-class ProcessCacheLRUTestable extends ProcessCacheLRU {
-       public function getCache() {
-               return $this->cache->toArray();
-       }
-
-       public function getEntriesCount() {
-               return count( $this->cache->toArray() );
-       }
-}
diff --git a/tests/phpunit/includes/libs/SamplingStatsdClientTest.php b/tests/phpunit/includes/libs/SamplingStatsdClientTest.php
deleted file mode 100644 (file)
index 7bd1611..0000000
+++ /dev/null
@@ -1,77 +0,0 @@
-<?php
-
-use Liuggio\StatsdClient\Entity\StatsdData;
-use Liuggio\StatsdClient\Sender\SenderInterface;
-
-/**
- * @covers SamplingStatsdClient
- */
-class SamplingStatsdClientTest extends PHPUnit\Framework\TestCase {
-
-       use MediaWikiCoversValidator;
-
-       /**
-        * @dataProvider samplingDataProvider
-        */
-       public function testSampling( $data, $sampleRate, $seed, $expectWrite ) {
-               $sender = $this->getMockBuilder( SenderInterface::class )->getMock();
-               $sender->expects( $this->any() )->method( 'open' )->will( $this->returnValue( true ) );
-               if ( $expectWrite ) {
-                       $sender->expects( $this->once() )->method( 'write' )
-                               ->with( $this->anything(), $this->equalTo( $data ) );
-               } else {
-                       $sender->expects( $this->never() )->method( 'write' );
-               }
-               if ( defined( 'MT_RAND_PHP' ) ) {
-                       mt_srand( $seed, MT_RAND_PHP );
-               } else {
-                       mt_srand( $seed );
-               }
-               $client = new SamplingStatsdClient( $sender );
-               $client->send( $data, $sampleRate );
-       }
-
-       public function samplingDataProvider() {
-               $unsampled = new StatsdData();
-               $unsampled->setKey( 'foo' );
-               $unsampled->setValue( 1 );
-
-               $sampled = new StatsdData();
-               $sampled->setKey( 'foo' );
-               $sampled->setValue( 1 );
-               $sampled->setSampleRate( '0.1' );
-
-               return [
-                       // $data, $sampleRate, $seed, $expectWrite
-                       [ $unsampled, 1, 0 /*0.44*/, true ],
-                       [ $sampled, 1, 0 /*0.44*/, false ],
-                       [ $sampled, 1, 4 /*0.03*/, true ],
-                       [ $unsampled, 0.1, 0 /*0.44*/, false ],
-                       [ $sampled, 0.5, 0 /*0.44*/, false ],
-                       [ $sampled, 0.5, 4 /*0.03*/, false ],
-               ];
-       }
-
-       public function testSetSamplingRates() {
-               $matching = new StatsdData();
-               $matching->setKey( 'foo.bar' );
-               $matching->setValue( 1 );
-
-               $nonMatching = new StatsdData();
-               $nonMatching->setKey( 'oof.bar' );
-               $nonMatching->setValue( 1 );
-
-               $sender = $this->getMockBuilder( SenderInterface::class )->getMock();
-               $sender->expects( $this->any() )->method( 'open' )->will( $this->returnValue( true ) );
-               $sender->expects( $this->once() )->method( 'write' )->with( $this->anything(),
-                       $this->equalTo( $nonMatching ) );
-
-               $client = new SamplingStatsdClient( $sender );
-               $client->setSamplingRates( [ 'foo.*' => 0.2 ] );
-
-               mt_srand( 0 ); // next random is 0.44
-               $client->send( $matching );
-               mt_srand( 0 );
-               $client->send( $nonMatching );
-       }
-}
diff --git a/tests/phpunit/includes/libs/StaticArrayWriterTest.php b/tests/phpunit/includes/libs/StaticArrayWriterTest.php
deleted file mode 100644 (file)
index 4bd845d..0000000
+++ /dev/null
@@ -1,58 +0,0 @@
-<?php
-/**
- * Copyright (C) 2018 Kunal Mehta <legoktm@member.fsf.org>
- *
- * 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; either version 2 of the License, or
- * (at your option) any later version.
- *
- * 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.
- *
- */
-
-use Wikimedia\StaticArrayWriter;
-
-/**
- * @covers \Wikimedia\StaticArrayWriter
- */
-class StaticArrayWriterTest extends PHPUnit\Framework\TestCase {
-       public function testCreate() {
-               $data = [
-                       'foo' => 'bar',
-                       'baz' => 'rawr',
-                       "they're" => '"quoted properly"',
-                       'nested' => [ 'elements', 'work' ],
-                       'and' => [ 'these' => 'do too' ],
-               ];
-               $writer = new StaticArrayWriter();
-               $actual = $writer->create( $data, "Header\nWith\nNewlines" );
-               $expected = <<<PHP
-<?php
-// Header
-// With
-// Newlines
-return [
-       'foo' => 'bar',
-       'baz' => 'rawr',
-       'they\'re' => '"quoted properly"',
-       'nested' => [
-               0 => 'elements',
-               1 => 'work',
-       ],
-       'and' => [
-               'these' => 'do too',
-       ],
-];
-
-PHP;
-               $this->assertSame( $expected, $actual );
-       }
-}
diff --git a/tests/phpunit/includes/libs/StringUtilsTest.php b/tests/phpunit/includes/libs/StringUtilsTest.php
deleted file mode 100644 (file)
index fcfa53e..0000000
+++ /dev/null
@@ -1,128 +0,0 @@
-<?php
-
-class StringUtilsTest extends PHPUnit\Framework\TestCase {
-
-       use MediaWikiCoversValidator;
-
-       /**
-        * @covers StringUtils::isUtf8
-        * @dataProvider provideStringsForIsUtf8Check
-        */
-       public function testIsUtf8( $expected, $string ) {
-               $this->assertEquals( $expected, StringUtils::isUtf8( $string ),
-                       'Testing string "' . $this->escaped( $string ) . '"' );
-       }
-
-       /**
-        * Print high range characters as a hexadecimal
-        * @param string $string
-        * @return string
-        */
-       function escaped( $string ) {
-               $escaped = '';
-               $length = strlen( $string );
-               for ( $i = 0; $i < $length; $i++ ) {
-                       $char = $string[$i];
-                       $val = ord( $char );
-                       if ( $val > 127 ) {
-                               $escaped .= '\x' . dechex( $val );
-                       } else {
-                               $escaped .= $char;
-                       }
-               }
-
-               return $escaped;
-       }
-
-       /**
-        * See also "UTF-8 decoder capability and stress test" by
-        * Markus Kuhn:
-        * http://www.cl.cam.ac.uk/~mgk25/ucs/examples/UTF-8-test.txt
-        */
-       public static function provideStringsForIsUtf8Check() {
-               // Expected return values for StringUtils::isUtf8()
-               $PASS = true;
-               $FAIL = false;
-
-               return [
-                       'some ASCII' => [ $PASS, 'Some ASCII' ],
-                       'euro sign' => [ $PASS, "Euro sign €" ],
-
-                       'first possible sequence 1 byte' => [ $PASS, "\x00" ],
-                       'first possible sequence 2 bytes' => [ $PASS, "\xc2\x80" ],
-                       'first possible sequence 3 bytes' => [ $PASS, "\xe0\xa0\x80" ],
-                       'first possible sequence 4 bytes' => [ $PASS, "\xf0\x90\x80\x80" ],
-                       'first possible sequence 5 bytes' => [ $FAIL, "\xf8\x88\x80\x80\x80" ],
-                       'first possible sequence 6 bytes' => [ $FAIL, "\xfc\x84\x80\x80\x80\x80" ],
-
-                       'last possible sequence 1 byte' => [ $PASS, "\x7f" ],
-                       'last possible sequence 2 bytes' => [ $PASS, "\xdf\xbf" ],
-                       'last possible sequence 3 bytes' => [ $PASS, "\xef\xbf\xbf" ],
-                       'last possible sequence 4 bytes (U+1FFFFF)' => [ $FAIL, "\xf7\xbf\xbf\xbf" ],
-                       'last possible sequence 5 bytes' => [ $FAIL, "\xfb\xbf\xbf\xbf\xbf" ],
-                       'last possible sequence 6 bytes' => [ $FAIL, "\xfd\xbf\xbf\xbf\xbf\xbf" ],
-
-                       'boundary 1' => [ $PASS, "\xed\x9f\xbf" ],
-                       'boundary 2' => [ $PASS, "\xee\x80\x80" ],
-                       'boundary 3' => [ $PASS, "\xef\xbf\xbd" ],
-                       'boundary 4' => [ $PASS, "\xf2\x80\x80\x80" ],
-                       'boundary 5 (U+FFFFF)' => [ $PASS, "\xf3\xbf\xbf\xbf" ],
-                       'boundary 6 (U+100000)' => [ $PASS, "\xf4\x80\x80\x80" ],
-                       'boundary 7 (U+10FFFF)' => [ $PASS, "\xf4\x8f\xbf\xbf" ],
-                       'boundary 8 (U+110000)' => [ $FAIL, "\xf4\x90\x80\x80" ],
-
-                       'malformed 1' => [ $FAIL, "\x80" ],
-                       'malformed 2' => [ $FAIL, "\xbf" ],
-                       'malformed 3' => [ $FAIL, "\x80\xbf" ],
-                       'malformed 4' => [ $FAIL, "\x80\xbf\x80" ],
-                       'malformed 5' => [ $FAIL, "\x80\xbf\x80\xbf" ],
-                       'malformed 6' => [ $FAIL, "\x80\xbf\x80\xbf\x80" ],
-                       'malformed 7' => [ $FAIL, "\x80\xbf\x80\xbf\x80\xbf" ],
-                       'malformed 8' => [ $FAIL, "\x80\xbf\x80\xbf\x80\xbf\x80" ],
-
-                       'last byte missing 1' => [ $FAIL, "\xc0" ],
-                       'last byte missing 2' => [ $FAIL, "\xe0\x80" ],
-                       'last byte missing 3' => [ $FAIL, "\xf0\x80\x80" ],
-                       'last byte missing 4' => [ $FAIL, "\xf8\x80\x80\x80" ],
-                       'last byte missing 5' => [ $FAIL, "\xfc\x80\x80\x80\x80" ],
-                       'last byte missing 6' => [ $FAIL, "\xdf" ],
-                       'last byte missing 7' => [ $FAIL, "\xef\xbf" ],
-                       'last byte missing 8' => [ $FAIL, "\xf7\xbf\xbf" ],
-                       'last byte missing 9' => [ $FAIL, "\xfb\xbf\xbf\xbf" ],
-                       'last byte missing 10' => [ $FAIL, "\xfd\xbf\xbf\xbf\xbf" ],
-
-                       'extra continuation byte 1' => [ $FAIL, "e\xaf" ],
-                       'extra continuation byte 2' => [ $FAIL, "\xc3\x89\xaf" ],
-                       'extra continuation byte 3' => [ $FAIL, "\xef\xbc\xa5\xaf" ],
-                       'extra continuation byte 4' => [ $FAIL, "\xf0\x9d\x99\xb4\xaf" ],
-
-                       'impossible bytes 1' => [ $FAIL, "\xfe" ],
-                       'impossible bytes 2' => [ $FAIL, "\xff" ],
-                       'impossible bytes 3' => [ $FAIL, "\xfe\xfe\xff\xff" ],
-
-                       'overlong sequences 1' => [ $FAIL, "\xc0\xaf" ],
-                       'overlong sequences 2' => [ $FAIL, "\xc1\xaf" ],
-                       'overlong sequences 3' => [ $FAIL, "\xe0\x80\xaf" ],
-                       'overlong sequences 4' => [ $FAIL, "\xf0\x80\x80\xaf" ],
-                       'overlong sequences 5' => [ $FAIL, "\xf8\x80\x80\x80\xaf" ],
-                       'overlong sequences 6' => [ $FAIL, "\xfc\x80\x80\x80\x80\xaf" ],
-
-                       'maximum overlong sequences 1' => [ $FAIL, "\xc1\xbf" ],
-                       'maximum overlong sequences 2' => [ $FAIL, "\xe0\x9f\xbf" ],
-                       'maximum overlong sequences 3' => [ $FAIL, "\xf0\x8f\xbf\xbf" ],
-                       'maximum overlong sequences 4' => [ $FAIL, "\xf8\x87\xbf\xbf" ],
-                       'maximum overlong sequences 5' => [ $FAIL, "\xfc\x83\xbf\xbf\xbf\xbf" ],
-
-                       'surrogates 1 (U+D799)' => [ $PASS, "\xed\x9f\xbf" ],
-                       'surrogates 2 (U+E000)' => [ $PASS, "\xee\x80\x80" ],
-                       'surrogates 3 (U+D800)' => [ $FAIL, "\xed\xa0\x80" ],
-                       'surrogates 4 (U+DBFF)' => [ $FAIL, "\xed\xaf\xbf" ],
-                       'surrogates 5 (U+DC00)' => [ $FAIL, "\xed\xb0\x80" ],
-                       'surrogates 6 (U+DFFF)' => [ $FAIL, "\xed\xbf\xbf" ],
-                       'surrogates 7 (U+D800 U+DC00)' => [ $FAIL, "\xed\xa0\x80\xed\xb0\x80" ],
-
-                       'noncharacters 1' => [ $PASS, "\xef\xbf\xbe" ],
-                       'noncharacters 2' => [ $PASS, "\xef\xbf\xbf" ],
-               ];
-       }
-}
diff --git a/tests/phpunit/includes/libs/TimingTest.php b/tests/phpunit/includes/libs/TimingTest.php
deleted file mode 100644 (file)
index 581a518..0000000
+++ /dev/null
@@ -1,115 +0,0 @@
-<?php
-/**
- * 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; either version 2 of the License, or
- * (at your option) any later version.
- *
- * 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.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- * @author Ori Livneh <ori@wikimedia.org>
- */
-
-class TimingTest extends PHPUnit\Framework\TestCase {
-
-       use MediaWikiCoversValidator;
-
-       /**
-        * @covers Timing::clearMarks
-        * @covers Timing::getEntries
-        */
-       public function testClearMarks() {
-               $timing = new Timing;
-               $this->assertCount( 1, $timing->getEntries() );
-
-               $timing->mark( 'a' );
-               $timing->mark( 'b' );
-               $this->assertCount( 3, $timing->getEntries() );
-
-               $timing->clearMarks( 'a' );
-               $this->assertNull( $timing->getEntryByName( 'a' ) );
-               $this->assertNotNull( $timing->getEntryByName( 'b' ) );
-
-               $timing->clearMarks();
-               $this->assertCount( 1, $timing->getEntries() );
-       }
-
-       /**
-        * @covers Timing::mark
-        * @covers Timing::getEntryByName
-        */
-       public function testMark() {
-               $timing = new Timing;
-               $timing->mark( 'a' );
-
-               $entry = $timing->getEntryByName( 'a' );
-               $this->assertEquals( 'a', $entry['name'] );
-               $this->assertEquals( 'mark', $entry['entryType'] );
-               $this->assertArrayHasKey( 'startTime', $entry );
-               $this->assertEquals( 0, $entry['duration'] );
-
-               usleep( 100 );
-               $timing->mark( 'a' );
-               $newEntry = $timing->getEntryByName( 'a' );
-               $this->assertGreaterThan( $entry['startTime'], $newEntry['startTime'] );
-       }
-
-       /**
-        * @covers Timing::measure
-        */
-       public function testMeasure() {
-               $timing = new Timing;
-
-               $timing->mark( 'a' );
-               usleep( 100 );
-               $timing->mark( 'b' );
-
-               $a = $timing->getEntryByName( 'a' );
-               $b = $timing->getEntryByName( 'b' );
-
-               $timing->measure( 'a_to_b', 'a', 'b' );
-
-               $entry = $timing->getEntryByName( 'a_to_b' );
-               $this->assertEquals( 'a_to_b', $entry['name'] );
-               $this->assertEquals( 'measure', $entry['entryType'] );
-               $this->assertEquals( $a['startTime'], $entry['startTime'] );
-               $this->assertEquals( $b['startTime'] - $a['startTime'], $entry['duration'] );
-       }
-
-       /**
-        * @covers Timing::getEntriesByType
-        */
-       public function testGetEntriesByType() {
-               $timing = new Timing;
-
-               $timing->mark( 'mark_a' );
-               usleep( 100 );
-               $timing->mark( 'mark_b' );
-               usleep( 100 );
-               $timing->mark( 'mark_c' );
-
-               $timing->measure( 'measure_a', 'mark_a', 'mark_b' );
-               $timing->measure( 'measure_b', 'mark_b', 'mark_c' );
-
-               $marks = array_map( function ( $entry ) {
-                       return $entry['name'];
-               }, $timing->getEntriesByType( 'mark' ) );
-
-               $this->assertEquals( [ 'requestStart', 'mark_a', 'mark_b', 'mark_c' ], $marks );
-
-               $measures = array_map( function ( $entry ) {
-                       return $entry['name'];
-               }, $timing->getEntriesByType( 'measure' ) );
-
-               $this->assertEquals( [ 'measure_a', 'measure_b' ], $measures );
-       }
-}
diff --git a/tests/phpunit/includes/libs/XhprofDataTest.php b/tests/phpunit/includes/libs/XhprofDataTest.php
deleted file mode 100644 (file)
index 3e93794..0000000
+++ /dev/null
@@ -1,274 +0,0 @@
-<?php
-/**
- * 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; either version 2 of the License, or
- * (at your option) any later version.
- *
- * 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.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- */
-
-/**
- * @copyright © 2014 Wikimedia Foundation and contributors
- * @since 1.25
- */
-class XhprofDataTest extends PHPUnit\Framework\TestCase {
-
-       use MediaWikiCoversValidator;
-
-       /**
-        * @covers XhprofData::splitKey
-        * @dataProvider provideSplitKey
-        */
-       public function testSplitKey( $key, $expect ) {
-               $this->assertSame( $expect, XhprofData::splitKey( $key ) );
-       }
-
-       public function provideSplitKey() {
-               return [
-                       [ 'main()', [ null, 'main()' ] ],
-                       [ 'foo==>bar', [ 'foo', 'bar' ] ],
-                       [ 'bar@1==>bar@2', [ 'bar@1', 'bar@2' ] ],
-                       [ 'foo==>bar==>baz', [ 'foo', 'bar==>baz' ] ],
-                       [ '==>bar', [ '', 'bar' ] ],
-                       [ '', [ null, '' ] ],
-               ];
-       }
-
-       /**
-        * @covers XhprofData::pruneData
-        */
-       public function testInclude() {
-               $xhprofData = $this->getXhprofDataFixture( [
-                       'include' => [ 'main()' ],
-               ] );
-               $raw = $xhprofData->getRawData();
-               $this->assertArrayHasKey( 'main()', $raw );
-               $this->assertArrayHasKey( 'main()==>foo', $raw );
-               $this->assertArrayHasKey( 'main()==>xhprof_disable', $raw );
-               $this->assertSame( 3, count( $raw ) );
-       }
-
-       /**
-        * Validate the structure of data returned by
-        * Xhprof::getInclusiveMetrics(). This acts as a guard against unexpected
-        * structural changes to the returned data in lieu of using a more heavy
-        * weight typed response object.
-        *
-        * @covers XhprofData::getInclusiveMetrics
-        */
-       public function testInclusiveMetricsStructure() {
-               $metricStruct = [
-                       'ct' => 'int',
-                       'wt' => 'array',
-                       'cpu' => 'array',
-                       'mu' => 'array',
-                       'pmu' => 'array',
-               ];
-               $statStruct = [
-                       'total' => 'numeric',
-                       'min' => 'numeric',
-                       'mean' => 'numeric',
-                       'max' => 'numeric',
-                       'variance' => 'numeric',
-                       'percent' => 'numeric',
-               ];
-
-               $xhprofData = $this->getXhprofDataFixture();
-               $metrics = $xhprofData->getInclusiveMetrics();
-
-               foreach ( $metrics as $name => $metric ) {
-                       $this->assertArrayStructure( $metricStruct, $metric );
-
-                       foreach ( $metricStruct as $key => $type ) {
-                               if ( $type === 'array' ) {
-                                       $this->assertArrayStructure( $statStruct, $metric[$key] );
-                                       if ( $name === 'main()' ) {
-                                               $this->assertEquals( 100, $metric[$key]['percent'] );
-                                       }
-                               }
-                       }
-               }
-       }
-
-       /**
-        * Validate the structure of data returned by
-        * Xhprof::getCompleteMetrics(). This acts as a guard against unexpected
-        * structural changes to the returned data in lieu of using a more heavy
-        * weight typed response object.
-        *
-        * @covers XhprofData::getCompleteMetrics
-        */
-       public function testCompleteMetricsStructure() {
-               $metricStruct = [
-                       'ct' => 'int',
-                       'wt' => 'array',
-                       'cpu' => 'array',
-                       'mu' => 'array',
-                       'pmu' => 'array',
-                       'calls' => 'array',
-                       'subcalls' => 'array',
-               ];
-               $statsMetrics = [ 'wt', 'cpu', 'mu', 'pmu' ];
-               $statStruct = [
-                       'total' => 'numeric',
-                       'min' => 'numeric',
-                       'mean' => 'numeric',
-                       'max' => 'numeric',
-                       'variance' => 'numeric',
-                       'percent' => 'numeric',
-                       'exclusive' => 'numeric',
-               ];
-
-               $xhprofData = $this->getXhprofDataFixture();
-               $metrics = $xhprofData->getCompleteMetrics();
-
-               foreach ( $metrics as $name => $metric ) {
-                       $this->assertArrayStructure( $metricStruct, $metric, $name );
-
-                       foreach ( $metricStruct as $key => $type ) {
-                               if ( in_array( $key, $statsMetrics ) ) {
-                                       $this->assertArrayStructure(
-                                               $statStruct, $metric[$key], $key
-                                       );
-                                       $this->assertLessThanOrEqual(
-                                               $metric[$key]['total'], $metric[$key]['exclusive']
-                                       );
-                               }
-                       }
-               }
-       }
-
-       /**
-        * @covers XhprofData::getCallers
-        * @covers XhprofData::getCallees
-        */
-       public function testEdges() {
-               $xhprofData = $this->getXhprofDataFixture();
-               $this->assertSame( [], $xhprofData->getCallers( 'main()' ) );
-               $this->assertSame( [ 'foo', 'xhprof_disable' ],
-                       $xhprofData->getCallees( 'main()' )
-               );
-               $this->assertSame( [ 'main()' ],
-                       $xhprofData->getCallers( 'foo' )
-               );
-               $this->assertSame( [], $xhprofData->getCallees( 'strlen' ) );
-       }
-
-       /**
-        * @covers XhprofData::getCriticalPath
-        */
-       public function testCriticalPath() {
-               $xhprofData = $this->getXhprofDataFixture();
-               $path = $xhprofData->getCriticalPath();
-
-               $last = null;
-               foreach ( $path as $key => $value ) {
-                       list( $func, $call ) = XhprofData::splitKey( $key );
-                       $this->assertSame( $last, $func );
-                       $last = $call;
-               }
-               $this->assertSame( $last, 'bar@1' );
-       }
-
-       /**
-        * Get an Xhprof instance that has been primed with a set of known testing
-        * data. Tests for the Xhprof class should laregly be concerned with
-        * evaluating the manipulations of the data collected by xhprof rather
-        * than the data collection process itself.
-        *
-        * The returned Xhprof instance primed will be with a data set created by
-        * running this trivial program using the PECL xhprof implementation:
-        * @code
-        * function bar( $x ) {
-        *   if ( $x > 0 ) {
-        *     bar($x - 1);
-        *   }
-        * }
-        * function foo() {
-        *   for ( $idx = 0; $idx < 2; $idx++ ) {
-        *     bar( $idx );
-        *     $x = strlen( 'abc' );
-        *   }
-        * }
-        * xhprof_enable( XHPROF_FLAGS_CPU | XHPROF_FLAGS_MEMORY );
-        * foo();
-        * $x = xhprof_disable();
-        * var_export( $x );
-        * @endcode
-        *
-        * @return Xhprof
-        */
-       protected function getXhprofDataFixture( array $opts = [] ) {
-               return new XhprofData( [
-                       'foo==>bar' => [
-                               'ct' => 2,
-                               'wt' => 57,
-                               'cpu' => 92,
-                               'mu' => 1896,
-                               'pmu' => 0,
-                       ],
-                       'foo==>strlen' => [
-                               'ct' => 2,
-                               'wt' => 21,
-                               'cpu' => 141,
-                               'mu' => 752,
-                               'pmu' => 0,
-                       ],
-                       'bar==>bar@1' => [
-                               'ct' => 1,
-                               'wt' => 18,
-                               'cpu' => 19,
-                               'mu' => 752,
-                               'pmu' => 0,
-                       ],
-                       'main()==>foo' => [
-                               'ct' => 1,
-                               'wt' => 304,
-                               'cpu' => 307,
-                               'mu' => 4008,
-                               'pmu' => 0,
-                       ],
-                       'main()==>xhprof_disable' => [
-                               'ct' => 1,
-                               'wt' => 8,
-                               'cpu' => 10,
-                               'mu' => 768,
-                               'pmu' => 392,
-                       ],
-                       'main()' => [
-                               'ct' => 1,
-                               'wt' => 353,
-                               'cpu' => 351,
-                               'mu' => 6112,
-                               'pmu' => 1424,
-                       ],
-               ], $opts );
-       }
-
-       /**
-        * Assert that the given array has the described structure.
-        *
-        * @param array $struct Array of key => type mappings
-        * @param array $actual Array to check
-        * @param string $label
-        */
-       protected function assertArrayStructure( $struct, $actual, $label = null ) {
-               $this->assertInternalType( 'array', $actual, $label );
-               $this->assertCount( count( $struct ), $actual, $label );
-               foreach ( $struct as $key => $type ) {
-                       $this->assertArrayHasKey( $key, $actual );
-                       $this->assertInternalType( $type, $actual[$key] );
-               }
-       }
-}
diff --git a/tests/phpunit/includes/libs/XhprofTest.php b/tests/phpunit/includes/libs/XhprofTest.php
deleted file mode 100644 (file)
index ccad4a4..0000000
+++ /dev/null
@@ -1,113 +0,0 @@
-<?php
-/**
- * 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; either version 2 of the License, or
- * (at your option) any later version.
- *
- * 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.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- */
-
-class XhprofTest extends PHPUnit\Framework\TestCase {
-
-       use MediaWikiCoversValidator;
-
-       /**
-        * Trying to enable Xhprof when it is already enabled causes an exception
-        * to be thrown.
-        *
-        * @expectedException        Exception
-        * @expectedExceptionMessage already enabled
-        * @covers Xhprof::enable
-        */
-       public function testEnable() {
-               $xhprof = new ReflectionClass( Xhprof::class );
-               $enabled = $xhprof->getProperty( 'enabled' );
-               $enabled->setAccessible( true );
-               $enabled->setValue( true );
-               $xhprof->getMethod( 'enable' )->invoke( null );
-       }
-
-       /**
-        * callAny() calls the first function of the list.
-        *
-        * @covers Xhprof::callAny
-        * @dataProvider provideCallAny
-        */
-       public function testCallAny( array $functions, array $args, $expectedResult ) {
-               $xhprof = new ReflectionClass( Xhprof::class );
-               $callAny = $xhprof->getMethod( 'callAny' );
-               $callAny->setAccessible( true );
-
-               $this->assertEquals( $expectedResult,
-                       $callAny->invoke( null, $functions, $args ) );
-       }
-
-       /**
-        * Data provider for testCallAny().
-       */
-       public function provideCallAny() {
-               return [
-                       [
-                               [ 'wfTestCallAny_func1', 'wfTestCallAny_func2', 'wfTestCallAny_func3' ],
-                               [ 3, 4 ],
-                               12
-                       ],
-                       [
-                               [ 'wfTestCallAny_nosuchfunc1', 'wfTestCallAny_func2', 'wfTestCallAny_func3' ],
-                               [ 3, 4 ],
-                               7
-                       ],
-                       [
-                               [ 'wfTestCallAny_nosuchfunc1', 'wfTestCallAny_nosuchfunc2', 'wfTestCallAny_func3' ],
-                               [ 3, 4 ],
-                               -1
-                       ]
-
-               ];
-       }
-
-       /**
-        * callAny() throws an exception when all functions are unavailable.
-        *
-        * @expectedException        Exception
-        * @expectedExceptionMessage Neither xhprof nor tideways are installed
-        * @covers Xhprof::callAny
-        */
-       public function testCallAnyNoneAvailable() {
-               $xhprof = new ReflectionClass( Xhprof::class );
-               $callAny = $xhprof->getMethod( 'callAny' );
-               $callAny->setAccessible( true );
-
-               $callAny->invoke( $xhprof, [
-                       'wfTestCallAny_nosuchfunc1',
-                       'wfTestCallAny_nosuchfunc2',
-                       'wfTestCallAny_nosuchfunc3'
-               ] );
-       }
-}
-
-/** Test function #1 for XhprofTest::testCallAny */
-function wfTestCallAny_func1( $a, $b ) {
-       return $a * $b;
-}
-
-/** Test function #2 for XhprofTest::testCallAny */
-function wfTestCallAny_func2( $a, $b ) {
-       return $a + $b;
-}
-
-/** Test function #3 for XhprofTest::testCallAny */
-function wfTestCallAny_func3( $a, $b ) {
-       return $a - $b;
-}
diff --git a/tests/phpunit/includes/libs/XmlTypeCheckTest.php b/tests/phpunit/includes/libs/XmlTypeCheckTest.php
deleted file mode 100644 (file)
index 8616b41..0000000
+++ /dev/null
@@ -1,79 +0,0 @@
-<?php
-/**
- * PHPUnit tests for XMLTypeCheck.
- * @author physikerwelt
- * @group Xml
- * @covers XMLTypeCheck
- */
-class XmlTypeCheckTest extends PHPUnit\Framework\TestCase {
-
-       use MediaWikiCoversValidator;
-
-       const WELL_FORMED_XML = "<root><child /></root>";
-       const MAL_FORMED_XML = "<root><child /></error>";
-       // phpcs:ignore Generic.Files.LineLength
-       const XML_WITH_PIH = '<?xml version="1.0"?><?xml-stylesheet type="text/xsl" href="/w/index.php"?><svg><child /></svg>';
-
-       /**
-        * @covers XMLTypeCheck::newFromString
-        * @covers XMLTypeCheck::getRootElement
-        */
-       public function testWellFormedXML() {
-               $testXML = XmlTypeCheck::newFromString( self::WELL_FORMED_XML );
-               $this->assertTrue( $testXML->wellFormed );
-               $this->assertEquals( 'root', $testXML->getRootElement() );
-       }
-
-       /**
-        * @covers XMLTypeCheck::newFromString
-        */
-       public function testMalFormedXML() {
-               $testXML = XmlTypeCheck::newFromString( self::MAL_FORMED_XML );
-               $this->assertFalse( $testXML->wellFormed );
-       }
-
-       /**
-        * Verify we check for recursive entity DOS
-        *
-        * (If the DOS isn't properly handled, the test runner will probably go OOM...)
-        */
-       public function testRecursiveEntity() {
-               $xml = <<<'XML'
-<?xml version="1.0" encoding="utf-8"?>
-<!DOCTYPE foo [
-       <!ENTITY test "&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;">
-       <!ENTITY a "&b;&b;&b;&b;&b;&b;&b;&b;&b;&b;&b;&b;&b;&b;&b;&b;&b;&b;&b;&b;&b;&b;&b;&b;">
-       <!ENTITY b "&c;&c;&c;&c;&c;&c;&c;&c;&c;&c;&c;&c;&c;&c;&c;&c;&c;&c;&c;&c;&c;&c;&c;&c;">
-       <!ENTITY c "&d;&d;&d;&d;&d;&d;&d;&d;&d;&d;&d;&d;&d;&d;&d;&d;&d;&d;&d;&d;&d;&d;&d;&d;">
-       <!ENTITY d "&e;&e;&e;&e;&e;&e;&e;&e;&e;&e;&e;&e;&e;&e;&e;&e;&e;&e;&e;&e;&e;&e;&e;&e;">
-       <!ENTITY e "&f;&f;&f;&f;&f;&f;&f;&f;&f;&f;&f;&f;&f;&f;&f;&f;&f;&f;&f;&f;&f;&f;&f;&f;">
-       <!ENTITY f "&g;&g;&g;&g;&g;&g;&g;&g;&g;&g;&g;&g;&g;&g;&g;&g;&g;&g;&g;&g;&g;&g;&g;&g;">
-       <!ENTITY g "-00000000000000000000000000000000000000000000000000000000000000000000000-">
-]>
-<foo>
-<bar>&test;</bar>
-</foo>
-XML;
-               $check = XmlTypeCheck::newFromString( $xml );
-               $this->assertFalse( $check->wellFormed );
-       }
-
-       /**
-        * @covers XMLTypeCheck::processingInstructionHandler
-        */
-       public function testProcessingInstructionHandler() {
-               $called = false;
-               $testXML = new XmlTypeCheck(
-                       self::XML_WITH_PIH,
-                       null,
-                       false,
-                       [
-                               'processing_instruction_handler' => function () use ( &$called ) {
-                                       $called = true;
-                               }
-                       ]
-               );
-               $this->assertTrue( $called );
-       }
-
-}
diff --git a/tests/phpunit/includes/libs/composer/ComposerInstalledTest.php b/tests/phpunit/includes/libs/composer/ComposerInstalledTest.php
deleted file mode 100644 (file)
index 58e617c..0000000
+++ /dev/null
@@ -1,498 +0,0 @@
-<?php
-
-class ComposerInstalledTest extends PHPUnit\Framework\TestCase {
-
-       private $installed;
-
-       public function setUp() {
-               parent::setUp();
-               $this->installed = __DIR__ . "/../../../data/composer/installed.json";
-       }
-
-       /**
-        * @covers ComposerInstalled::__construct
-        * @covers ComposerInstalled::getInstalledDependencies
-        */
-       public function testGetInstalledDependencies() {
-               $installed = new ComposerInstalled( $this->installed );
-               $this->assertEquals( [
-               'leafo/lessphp' => [
-                       'version' => '0.5.0',
-                       'type' => 'library',
-                       'licenses' => [ 'MIT', 'GPL-3.0-only' ],
-                       'authors' => [
-                               [
-                                       'name' => 'Leaf Corcoran',
-                                       'email' => 'leafot@gmail.com',
-                                       'homepage' => 'http://leafo.net',
-                               ],
-                       ],
-                       'description' => 'lessphp is a compiler for LESS written in PHP.',
-               ],
-               'psr/log' => [
-                       'version' => '1.0.0',
-                       'type' => 'library',
-                       'licenses' => [ 'MIT' ],
-                       'authors' => [
-                               [
-                                       'name' => 'PHP-FIG',
-                                       'homepage' => 'http://www.php-fig.org/',
-                               ],
-                       ],
-                       'description' => 'Common interface for logging libraries',
-               ],
-               'cssjanus/cssjanus' => [
-                       'version' => '1.1.1',
-                       'type' => 'library',
-                       'licenses' => [ 'Apache-2.0' ],
-                       'authors' => [
-                       ],
-                       'description' => 'Convert CSS stylesheets between left-to-right ' .
-                               'and right-to-left.',
-               ],
-               'cdb/cdb' => [
-                       'version' => '1.0.0',
-                       'type' => 'library',
-                       'licenses' => [ 'GPLv2' ],
-                       'authors' => [
-                               [
-                                       'name' => 'Tim Starling',
-                                       'email' => 'tstarling@wikimedia.org',
-                               ],
-                               [
-                                       'name' => 'Chad Horohoe',
-                                       'email' => 'chad@wikimedia.org',
-                               ],
-                       ],
-                       'description' => 'Constant Database (CDB) wrapper library for PHP. ' .
-                               'Provides pure-PHP fallback when dba_* functions are absent.',
-               ],
-               'sebastian/version' => [
-                       'version' => '2.0.1',
-                       'type' => 'library',
-                       'licenses' => [ 'BSD-3-Clause' ],
-                       'authors' => [
-                               [
-                                       'name' => 'Sebastian Bergmann',
-                                       'email' => 'sebastian@phpunit.de',
-                                       'role' => 'lead',
-                               ],
-                       ],
-                       'description' => 'Library that helps with managing the version ' .
-                               'number of Git-hosted PHP projects',
-               ],
-               'sebastian/resource-operations' => [
-                       'version' => '1.0.0',
-                       'type' => 'library',
-                       'licenses' => [ 'BSD-3-Clause' ],
-                       'authors' => [
-                               [
-                                       'name' => 'Sebastian Bergmann',
-                                       'email' => 'sebastian@phpunit.de',
-                               ],
-                       ],
-                       'description' => 'Provides a list of PHP built-in functions that ' .
-                               'operate on resources',
-               ],
-               'sebastian/recursion-context' => [
-                       'version' => '3.0.0',
-                       'type' => 'library',
-                       'licenses' => [ 'BSD-3-Clause' ],
-                       'authors' => [
-                               [
-                                       'name' => 'Jeff Welch',
-                                       'email' => 'whatthejeff@gmail.com',
-                               ],
-                               [
-                                       'name' => 'Sebastian Bergmann',
-                                       'email' => 'sebastian@phpunit.de',
-                               ],
-                               [
-                                       'name' => 'Adam Harvey',
-                                       'email' => 'aharvey@php.net',
-                               ],
-                       ],
-                       'description' => 'Provides functionality to recursively process PHP ' .
-                               'variables',
-               ],
-               'sebastian/object-reflector' => [
-                       'version' => '1.1.1',
-                       'type' => 'library',
-                       'licenses' => [ 'BSD-3-Clause' ],
-                       'authors' => [
-                               [
-                                       'name' => 'Sebastian Bergmann',
-                                       'email' => 'sebastian@phpunit.de',
-                               ],
-                       ],
-                       'description' => 'Allows reflection of object attributes, including ' .
-                               'inherited and non-public ones',
-               ],
-               'sebastian/object-enumerator' => [
-                       'version' => '3.0.3',
-                       'type' => 'library',
-                       'licenses' => [ 'BSD-3-Clause' ],
-                       'authors' => [
-                               [
-                                       'name' => 'Sebastian Bergmann',
-                                       'email' => 'sebastian@phpunit.de',
-                               ],
-                       ],
-                       'description' => 'Traverses array structures and object graphs ' .
-                               'to enumerate all referenced objects',
-               ],
-               'sebastian/global-state' => [
-                       'version' => '2.0.0',
-                       'type' => 'library',
-                       'licenses' => [ 'BSD-3-Clause' ],
-                       'authors' => [
-                               [
-                                       'name' => 'Sebastian Bergmann',
-                                       'email' => 'sebastian@phpunit.de',
-                               ],
-                       ],
-                       'description' => 'Snapshotting of global state',
-               ],
-               'sebastian/exporter' => [
-                       'version' => '3.1.0',
-                       'type' => 'library',
-                       'licenses' => [ 'BSD-3-Clause' ],
-                       'authors' => [
-                               [
-                                       'name' => 'Jeff Welch',
-                                       'email' => 'whatthejeff@gmail.com',
-                               ],
-                               [
-                                       'name' => 'Volker Dusch',
-                                       'email' => 'github@wallbash.com',
-                               ],
-                               [
-                                       'name' => 'Bernhard Schussek',
-                                       'email' => 'bschussek@2bepublished.at',
-                               ],
-                               [
-                                       'name' => 'Sebastian Bergmann',
-                                       'email' => 'sebastian@phpunit.de',
-                               ],
-                               [
-                                       'name' => 'Adam Harvey',
-                                       'email' => 'aharvey@php.net',
-                               ],
-                       ],
-                       'description' => 'Provides the functionality to export PHP ' .
-                               'variables for visualization',
-               ],
-               'sebastian/environment' => [
-                       'version' => '3.1.0',
-                       'type' => 'library',
-                       'licenses' => [ 'BSD-3-Clause' ],
-                       'authors' => [
-                               [
-                                       'name' => 'Sebastian Bergmann',
-                                       'email' => 'sebastian@phpunit.de',
-                               ],
-                       ],
-                       'description' => 'Provides functionality to handle HHVM/PHP ' .
-                               'environments',
-               ],
-               'sebastian/diff' => [
-                       'version' => '2.0.1',
-                       'type' => 'library',
-                       'licenses' => [ 'BSD-3-Clause' ],
-                       'authors' => [
-                               [
-                                       'name' => 'Kore Nordmann',
-                                       'email' => 'mail@kore-nordmann.de',
-                               ],
-                               [
-                                       'name' => 'Sebastian Bergmann',
-                                       'email' => 'sebastian@phpunit.de',
-                               ],
-                       ],
-                       'description' => 'Diff implementation',
-               ],
-               'sebastian/comparator' => [
-                       'version' => '2.1.1',
-                       'type' => 'library',
-                       'licenses' => [ 'BSD-3-Clause' ],
-                       'authors' => [
-                               [
-                                       'name' => 'Jeff Welch',
-                                       'email' => 'whatthejeff@gmail.com',
-                               ],
-                               [
-                                       'name' => 'Volker Dusch',
-                                       'email' => 'github@wallbash.com',
-                               ],
-                               [
-                                       'name' => 'Bernhard Schussek',
-                                       'email' => 'bschussek@2bepublished.at',
-                               ],
-                               [
-                                       'name' => 'Sebastian Bergmann',
-                                       'email' => 'sebastian@phpunit.de',
-                               ],
-                       ],
-                       'description' => 'Provides the functionality to compare PHP ' .
-                               'values for equality',
-               ],
-               'doctrine/instantiator' => [
-                       'version' => '1.1.0',
-                       'type' => 'library',
-                       'licenses' => [ 'MIT' ],
-                       'authors' => [
-                               [
-                                       'name' => 'Marco Pivetta',
-                                       'email' => 'ocramius@gmail.com',
-                                       'homepage' => 'http://ocramius.github.com/',
-                               ],
-                       ],
-                       'description' => 'A small, lightweight utility to instantiate ' .
-                               'objects in PHP without invoking their constructors',
-               ],
-               'phpunit/php-text-template' => [
-                       'version' => '1.2.1',
-                       'type' => 'library',
-                       'licenses' => [ 'BSD-3-Clause' ],
-                       'authors' => [
-                               [
-                                       'name' => 'Sebastian Bergmann',
-                                       'email' => 'sebastian@phpunit.de',
-                                       'role' => 'lead',
-                               ],
-                       ],
-                       'description' => 'Simple template engine.',
-               ],
-               'phpunit/phpunit-mock-objects' => [
-                       'version' => '5.0.6',
-                       'type' => 'library',
-                       'licenses' => [ 'BSD-3-Clause' ],
-                       'authors' => [
-                               [
-                                       'name' => 'Sebastian Bergmann',
-                                       'email' => 'sebastian@phpunit.de',
-                                       'role' => 'lead',
-                               ],
-                       ],
-                       'description' => 'Mock Object library for PHPUnit',
-               ],
-               'phpunit/php-timer' => [
-                       'version' => '1.0.9',
-                       'type' => 'library',
-                       'licenses' => [ 'BSD-3-Clause' ],
-                       'authors' => [
-                               [
-                                       'name' => 'Sebastian Bergmann',
-                                       'email' => 'sb@sebastian-bergmann.de',
-                                       'role' => 'lead',
-                               ],
-                       ],
-                       'description' => 'Utility class for timing',
-               ],
-               'phpunit/php-file-iterator' => [
-                       'version' => '1.4.5',
-                       'type' => 'library',
-                       'licenses' => [ 'BSD-3-Clause' ],
-                       'authors' => [
-                               [
-                                       'name' => 'Sebastian Bergmann',
-                                       'email' => 'sb@sebastian-bergmann.de',
-                                       'role' => 'lead',
-                               ],
-                       ],
-                       'description' => 'FilterIterator implementation that filters ' .
-                               'files based on a list of suffixes.',
-               ],
-               'theseer/tokenizer' => [
-                       'version' => '1.1.0',
-                       'type' => 'library',
-                       'licenses' => [ 'BSD-3-Clause' ],
-                       'authors' => [
-                               [
-                                       'name' => 'Arne Blankerts',
-                                       'email' => 'arne@blankerts.de',
-                                       'role' => 'Developer',
-                               ],
-                       ],
-                       'description' => 'A small library for converting tokenized PHP ' .
-                               'source code into XML and potentially other formats',
-               ],
-               'sebastian/code-unit-reverse-lookup' => [
-                       'version' => '1.0.1',
-                       'type' => 'library',
-                       'licenses' => [ 'BSD-3-Clause' ],
-                       'authors' => [
-                               [
-                                       'name' => 'Sebastian Bergmann',
-                                       'email' => 'sebastian@phpunit.de',
-                               ],
-                       ],
-                       'description' => 'Looks up which function or method a line of ' .
-                               'code belongs to',
-               ],
-               'phpunit/php-token-stream' => [
-                       'version' => '2.0.2',
-                       'type' => 'library',
-                       'licenses' => [ 'BSD-3-Clause' ],
-                       'authors' => [
-                               [
-                                       'name' => 'Sebastian Bergmann',
-                                       'email' => 'sebastian@phpunit.de',
-                               ],
-                       ],
-                       'description' => 'Wrapper around PHP\'s tokenizer extension.',
-               ],
-               'phpunit/php-code-coverage' => [
-                       'version' => '5.3.0',
-                       'type' => 'library',
-                       'licenses' => [ 'BSD-3-Clause' ],
-                       'authors' => [
-                               [
-                                       'name' => 'Sebastian Bergmann',
-                                       'email' => 'sebastian@phpunit.de',
-                                       'role' => 'lead',
-                               ],
-                       ],
-                       'description' => 'Library that provides collection, processing, ' .
-                               'and rendering functionality for PHP code coverage information.',
-               ],
-               'webmozart/assert' => [
-                       'version' => '1.2.0',
-                       'type' => 'library',
-                       'licenses' => [ 'MIT' ],
-                       'authors' => [
-                               [
-                                       'name' => 'Bernhard Schussek',
-                                       'email' => 'bschussek@gmail.com',
-                               ],
-                       ],
-                       'description' => 'Assertions to validate method input/output with ' .
-                               'nice error messages.',
-               ],
-               'phpdocumentor/reflection-common' => [
-                       'version' => '1.0.1',
-                       'type' => 'library',
-                       'licenses' => [ 'MIT' ],
-                       'authors' => [
-                               [
-                                       'name' => 'Jaap van Otterdijk',
-                                       'email' => 'opensource@ijaap.nl',
-                               ],
-                       ],
-                       'description' => 'Common reflection classes used by phpdocumentor to ' .
-                               'reflect the code structure',
-               ],
-               'phpdocumentor/type-resolver' => [
-                       'version' => '0.4.0',
-                       'type' => 'library',
-                       'licenses' => [ 'MIT' ],
-                       'authors' => [
-                               [
-                                       'name' => 'Mike van Riel',
-                                       'email' => 'me@mikevanriel.com',
-                               ],
-                       ],
-                       'description' => '',
-               ],
-               'phpdocumentor/reflection-docblock' => [
-                       'version' => '4.2.0',
-                       'type' => 'library',
-                       'licenses' => [ 'MIT' ],
-                       'authors' => [
-                               [
-                                       'name' => 'Mike van Riel',
-                                       'email' => 'me@mikevanriel.com',
-                               ],
-                       ],
-                       'description' => 'With this component, a library can provide support for ' .
-                               'annotations via DocBlocks or otherwise retrieve information that ' .
-                               'is embedded in a DocBlock.',
-               ],
-               'phpspec/prophecy' => [
-                       'version' => '1.7.3',
-                       'type' => 'library',
-                       'licenses' => [ 'MIT' ],
-                       'authors' => [
-                               [
-                                       'name' => 'Konstantin Kudryashov',
-                                       'email' => 'ever.zet@gmail.com',
-                                       'homepage' => 'http://everzet.com',
-                               ],
-                               [
-                                       'name' => 'Marcello Duarte',
-                                       'email' => 'marcello.duarte@gmail.com',
-                               ],
-                       ],
-                       'description' => 'Highly opinionated mocking framework for PHP 5.3+',
-               ],
-               'phar-io/version' => [
-                       'version' => '1.0.1',
-                       'type' => 'library',
-                       'licenses' => [ 'BSD-3-Clause' ],
-                       'authors' => [
-                               [
-                                       'name' => 'Arne Blankerts',
-                                       'email' => 'arne@blankerts.de',
-                                       'role' => 'Developer',
-                               ],
-                               [
-                                       'name' => 'Sebastian Heuer',
-                                       'email' => 'sebastian@phpeople.de',
-                                       'role' => 'Developer',
-                               ],
-                               [
-                                       'name' => 'Sebastian Bergmann',
-                                       'email' => 'sebastian@phpunit.de',
-                                       'role' => 'Developer',
-                               ],
-                       ],
-                       'description' => 'Library for handling version information and constraints',
-               ],
-               'phar-io/manifest' => [
-                       'version' => '1.0.1',
-                       'type' => 'library',
-                       'licenses' => [ 'BSD-3-Clause' ],
-                       'authors' => [
-                               [
-                                       'name' => 'Arne Blankerts',
-                                       'email' => 'arne@blankerts.de',
-                                       'role' => 'Developer',
-                               ],
-                               [
-                                       'name' => 'Sebastian Heuer',
-                                       'email' => 'sebastian@phpeople.de',
-                                       'role' => 'Developer',
-                               ],
-                               [
-                                       'name' => 'Sebastian Bergmann',
-                                       'email' => 'sebastian@phpunit.de',
-                                       'role' => 'Developer',
-                               ],
-                       ],
-                       'description' => 'Component for reading phar.io manifest ' .
-                               'information from a PHP Archive (PHAR)',
-               ],
-               'myclabs/deep-copy' => [
-                       'version' => '1.7.0',
-                       'type' => 'library',
-                       'licenses' => [ 'MIT' ],
-                       'authors' => [
-                       ],
-                       'description' => 'Create deep copies (clones) of your objects',
-               ],
-               'phpunit/phpunit' => [
-                       'version' => '6.5.5',
-                       'type' => 'library',
-                       'licenses' => [ 'BSD-3-Clause' ],
-                       'authors' => [
-                               [
-                                       'name' => 'Sebastian Bergmann',
-                                       'email' => 'sebastian@phpunit.de',
-                                       'role' => 'lead',
-                               ],
-                       ],
-                       'description' => 'The PHP Unit Testing framework.',
-               ],
-               ], $installed->getInstalledDependencies() );
-       }
-}
diff --git a/tests/phpunit/includes/libs/composer/ComposerJsonTest.php b/tests/phpunit/includes/libs/composer/ComposerJsonTest.php
deleted file mode 100644 (file)
index 720fa6e..0000000
+++ /dev/null
@@ -1,41 +0,0 @@
-<?php
-
-class ComposerJsonTest extends PHPUnit\Framework\TestCase {
-
-       private $json, $json2;
-
-       public function setUp() {
-               parent::setUp();
-               $this->json = __DIR__ . "/../../../data/composer/composer.json";
-               $this->json2 = __DIR__ . "/../../../data/composer/new-composer.json";
-       }
-
-       /**
-        * @covers ComposerJson::__construct
-        * @covers ComposerJson::getRequiredDependencies
-        */
-       public function testGetRequiredDependencies() {
-               $json = new ComposerJson( $this->json );
-               $this->assertEquals( [
-                       'cdb/cdb' => '1.0.0',
-                       'cssjanus/cssjanus' => '1.1.1',
-                       'leafo/lessphp' => '0.5.0',
-                       'psr/log' => '1.0.0',
-               ], $json->getRequiredDependencies() );
-       }
-
-       public static function provideNormalizeVersion() {
-               return [
-                       [ 'v1.0.0', '1.0.0' ],
-                       [ '0.0.5', '0.0.5' ],
-               ];
-       }
-
-       /**
-        * @dataProvider provideNormalizeVersion
-        * @covers ComposerJson::normalizeVersion
-        */
-       public function testNormalizeVersion( $input, $expected ) {
-               $this->assertEquals( $expected, ComposerJson::normalizeVersion( $input ) );
-       }
-}
diff --git a/tests/phpunit/includes/libs/composer/ComposerLockTest.php b/tests/phpunit/includes/libs/composer/ComposerLockTest.php
deleted file mode 100644 (file)
index f5fcdbe..0000000
+++ /dev/null
@@ -1,120 +0,0 @@
-<?php
-
-class ComposerLockTest extends PHPUnit\Framework\TestCase {
-
-       private $lock;
-
-       public function setUp() {
-               parent::setUp();
-               $this->lock = __DIR__ . "/../../../data/composer/composer.lock";
-       }
-
-       /**
-        * @covers ComposerLock::__construct
-        * @covers ComposerLock::getInstalledDependencies
-        */
-       public function testGetInstalledDependencies() {
-               $lock = new ComposerLock( $this->lock );
-               $this->assertEquals( [
-                       'wikimedia/cdb' => [
-                               'version' => '1.0.1',
-                               'type' => 'library',
-                               'licenses' => [ 'GPL-2.0-only' ],
-                               'authors' => [
-                                       [
-                                               'name' => 'Tim Starling',
-                                               'email' => 'tstarling@wikimedia.org',
-                                       ],
-                                       [
-                                               'name' => 'Chad Horohoe',
-                                               'email' => 'chad@wikimedia.org',
-                                       ],
-                               ],
-                               'description' => 'Constant Database (CDB) wrapper library for PHP. ' .
-                                       'Provides pure-PHP fallback when dba_* functions are absent.',
-                       ],
-                       'cssjanus/cssjanus' => [
-                               'version' => '1.1.1',
-                               'type' => 'library',
-                               'licenses' => [ 'Apache-2.0' ],
-                               'authors' => [],
-                               'description' => 'Convert CSS stylesheets between left-to-right and right-to-left.',
-                       ],
-                       'leafo/lessphp' => [
-                               'version' => '0.5.0',
-                               'type' => 'library',
-                               'licenses' => [ 'MIT', 'GPL-3.0-only' ],
-                               'authors' => [
-                                       [
-                                               'name' => 'Leaf Corcoran',
-                                               'email' => 'leafot@gmail.com',
-                                               'homepage' => 'http://leafo.net',
-                                       ],
-                               ],
-                               'description' => 'lessphp is a compiler for LESS written in PHP.',
-                       ],
-                       'psr/log' => [
-                               'version' => '1.0.0',
-                               'type' => 'library',
-                               'licenses' => [ 'MIT' ],
-                               'authors' => [
-                                       [
-                                               'name' => 'PHP-FIG',
-                                               'homepage' => 'http://www.php-fig.org/',
-                                       ],
-                               ],
-                               'description' => 'Common interface for logging libraries',
-                       ],
-                       'oojs/oojs-ui' => [
-                               'version' => '0.6.0',
-                               'type' => 'library',
-                               'licenses' => [ 'MIT' ],
-                               'authors' => [],
-                               'description' => '',
-                       ],
-                       'composer/installers' => [
-                               'version' => '1.0.19',
-                               'type' => 'composer-installer',
-                               'licenses' => [ 'MIT' ],
-                               'authors' => [
-                                       [
-                                               'name' => 'Kyle Robinson Young',
-                                               'email' => 'kyle@dontkry.com',
-                                               'homepage' => 'https://github.com/shama',
-                                       ],
-                               ],
-                               'description' => 'A multi-framework Composer library installer',
-                       ],
-                       'mediawiki/translate' => [
-                               'version' => '2014.12',
-                               'type' => 'mediawiki-extension',
-                               'licenses' => [ 'GPL-2.0-or-later' ],
-                               'authors' => [
-                                       [
-                                               'name' => 'Niklas Laxström',
-                                               'email' => 'niklas.laxstrom@gmail.com',
-                                               'role' => 'Lead nitpicker',
-                                       ],
-                                       [
-                                               'name' => 'Siebrand Mazeland',
-                                               'email' => 's.mazeland@xs4all.nl',
-                                               'role' => 'Developer',
-                                       ],
-                               ],
-                               'description' => 'The only standard solution to translate any kind ' .
-                                       'of text with an avant-garde web interface within MediaWiki, ' .
-                                       'including your documentation and software',
-                       ],
-                       'mediawiki/universal-language-selector' => [
-                               'version' => '2014.12',
-                               'type' => 'mediawiki-extension',
-                               'licenses' => [ 'GPL-2.0-or-later', 'MIT' ],
-                               'authors' => [],
-                               'description' => 'The primary aim is to allow users to select a language ' .
-                                       'and configure its support in an easy way. ' .
-                                       'Main features are language selection, input methods and web fonts.',
-                       ],
-               ], $lock->getInstalledDependencies() );
-       }
-
-}
diff --git a/tests/phpunit/includes/libs/http/HttpAcceptNegotiatorTest.php b/tests/phpunit/includes/libs/http/HttpAcceptNegotiatorTest.php
deleted file mode 100644 (file)
index 02eac11..0000000
+++ /dev/null
@@ -1,150 +0,0 @@
-<?php
-
-use Wikimedia\Http\HttpAcceptNegotiator;
-
-/**
- * @covers Wikimedia\Http\HttpAcceptNegotiator
- *
- * @author Daniel Kinzler
- */
-class HttpAcceptNegotiatorTest extends \PHPUnit\Framework\TestCase {
-
-       public function provideGetFirstSupportedValue() {
-               return [
-                       [ // #0: empty
-                               [], // supported
-                               [], // accepted
-                               null, // default
-                               null,  // expected
-                       ],
-                       [ // #1: simple
-                               [ 'text/foo', 'text/BAR', 'application/zuul' ], // supported
-                               [ 'text/xzy', 'text/bar' ], // accepted
-                               null, // default
-                               'text/BAR',  // expected
-                       ],
-                       [ // #2: default
-                               [ 'text/foo', 'text/BAR', 'application/zuul' ], // supported
-                               [ 'text/xzy', 'text/xoo' ], // accepted
-                               'X', // default
-                               'X',  // expected
-                       ],
-                       [ // #3: preference
-                               [ 'text/foo', 'text/bar', 'application/zuul' ], // supported
-                               [ 'text/xoo', 'text/BAR', 'text/foo' ], // accepted
-                               null, // default
-                               'text/bar',  // expected
-                       ],
-                       [ // #4: * wildcard
-                               [ 'text/foo', 'text/BAR', 'application/zuul' ], // supported
-                               [ 'text/xoo', '*' ], // accepted
-                               null, // default
-                               'text/foo',  // expected
-                       ],
-                       [ // #5: */* wildcard
-                               [ 'text/foo', 'text/BAR', 'application/zuul' ], // supported
-                               [ 'text/xoo', '*/*' ], // accepted
-                               null, // default
-                               'text/foo',  // expected
-                       ],
-                       [ // #6: text/* wildcard
-                               [ 'text/foo', 'text/BAR', 'application/zuul' ], // supported
-                               [ 'application/*', 'text/foo' ], // accepted
-                               null, // default
-                               'application/zuul',  // expected
-                       ],
-               ];
-       }
-
-       /**
-        * @dataProvider provideGetFirstSupportedValue
-        */
-       public function testGetFirstSupportedValue( $supported, $accepted, $default, $expected ) {
-               $negotiator = new HttpAcceptNegotiator( $supported );
-               $actual = $negotiator->getFirstSupportedValue( $accepted, $default );
-
-               $this->assertEquals( $expected, $actual );
-       }
-
-       public function provideGetBestSupportedKey() {
-               return [
-                       [ // #0: empty
-                               [], // supported
-                               [], // accepted
-                               null, // default
-                               null,  // expected
-                       ],
-                       [ // #1: simple
-                               [ 'text/foo', 'text/BAR', 'application/zuul' ], // supported
-                               [ 'text/xzy' => 1, 'text/bar' => 0.5 ], // accepted
-                               null, // default
-                               'text/BAR',  // expected
-                       ],
-                       [ // #2: default
-                               [ 'text/foo', 'text/BAR', 'application/zuul' ], // supported
-                               [ 'text/xzy' => 1, 'text/xoo' => 0.5 ], // accepted
-                               'X', // default
-                               'X',  // expected
-                       ],
-                       [ // #3: weighted
-                               [ 'text/foo', 'text/BAR', 'application/zuul' ], // supported
-                               [ 'text/foo' => 0.3, 'text/BAR' => 0.8, 'application/zuul' => 0.5 ], // accepted
-                               null, // default
-                               'text/BAR',  // expected
-                       ],
-                       [ // #4: zero weight
-                               [ 'text/foo', 'text/BAR', 'application/zuul' ], // supported
-                               [ 'text/foo' => 0, 'text/xoo' => 1 ], // accepted
-                               null, // default
-                               null,  // expected
-                       ],
-                       [ // #5: * wildcard
-                               [ 'text/foo', 'text/BAR', 'application/zuul' ], // supported
-                               [ 'text/xoo' => 0.5, '*' => 0.1 ], // accepted
-                               null, // default
-                               'text/foo',  // expected
-                       ],
-                       [ // #6: */* wildcard
-                               [ 'text/foo', 'text/BAR', 'application/zuul' ], // supported
-                               [ 'text/xoo' => 0.5, '*/*' => 0.1 ], // accepted
-                               null, // default
-                               'text/foo',  // expected
-                       ],
-                       [ // #7: text/* wildcard
-                               [ 'text/foo', 'text/BAR', 'application/zuul' ], // supported
-                               [ 'text/foo' => 0.3, 'application/*' => 0.8 ], // accepted
-                               null, // default
-                               'application/zuul',  // expected
-                       ],
-                       [ // #8: Test specific format preferred over wildcard (T133314)
-                               [ 'application/rdf+xml', 'text/json', 'text/html' ], // supported
-                               [ '*/*' => 1, 'text/html' => 1 ], // accepted
-                               null, // default
-                               'text/html',  // expected
-                       ],
-                       [ // #9: Test specific format preferred over range (T133314)
-                               [ 'application/rdf+xml', 'text/json', 'text/html' ], // supported
-                               [ 'text/*' => 1, 'text/html' => 1 ], // accepted
-                               null, // default
-                               'text/html',  // expected
-                       ],
-                       [ // #10: Test range preferred over wildcard (T133314)
-                               [ 'application/rdf+xml', 'text/html' ], // supported
-                               [ '*/*' => 1, 'text/*' => 1 ], // accepted
-                               null, // default
-                               'text/html',  // expected
-                       ],
-               ];
-       }
-
-       /**
-        * @dataProvider provideGetBestSupportedKey
-        */
-       public function testGetBestSupportedKey( $supported, $accepted, $default, $expected ) {
-               $negotiator = new HttpAcceptNegotiator( $supported );
-               $actual = $negotiator->getBestSupportedKey( $accepted, $default );
-
-               $this->assertEquals( $expected, $actual );
-       }
-
-}
diff --git a/tests/phpunit/includes/libs/http/HttpAcceptParserTest.php b/tests/phpunit/includes/libs/http/HttpAcceptParserTest.php
deleted file mode 100644 (file)
index e4b47b4..0000000
+++ /dev/null
@@ -1,56 +0,0 @@
-<?php
-
-use Wikimedia\Http\HttpAcceptParser;
-
-/**
- * @covers Wikimedia\Http\HttpAcceptParser
- *
- * @author Daniel Kinzler
- */
-class HttpAcceptParserTest extends \PHPUnit\Framework\TestCase {
-
-       public function provideParseWeights() {
-               return [
-                       [ // #0
-                               '',
-                               []
-                       ],
-                       [ // #1
-                               'Foo/Bar',
-                               [ 'foo/bar' => 1 ]
-                       ],
-                       [ // #2
-                               'Accept: text/plain',
-                               [ 'text/plain' => 1 ]
-                       ],
-                       [ // #3
-                               'Accept: application/vnd.php.serialized, application/rdf+xml',
-                               [ 'application/vnd.php.serialized' => 1, 'application/rdf+xml' => 1 ]
-                       ],
-                       [ // #4
-                               'foo; q=0.2, xoo; q=0,text/n3',
-                               [ 'text/n3' => 1, 'foo' => 0.2 ]
-                       ],
-                       [ // #5
-                               '*; q=0.2, */*; q=0.1,text/*',
-                               [ 'text/*' => 1, '*' => 0.2, '*/*' => 0.1 ]
-                       ],
-                       // TODO: nicely ignore additional type paramerters
-                       //[ // #6
-                       //      'Foo; q=0.2, Xoo; level=3, Bar; charset=xyz; q=0.4',
-                       //      [ 'xoo' => 1, 'bar' => 0.4, 'foo' => 0.1 ]
-                       //],
-               ];
-       }
-
-       /**
-        * @dataProvider provideParseWeights
-        */
-       public function testParseWeights( $header, $expected ) {
-               $parser = new HttpAcceptParser();
-               $actual = $parser->parseWeights( $header );
-
-               $this->assertEquals( $expected, $actual ); // shouldn't be sensitive to order
-       }
-
-}
diff --git a/tests/phpunit/includes/libs/mime/MSCompoundFileReaderTest.php b/tests/phpunit/includes/libs/mime/MSCompoundFileReaderTest.php
deleted file mode 100644 (file)
index 4509a61..0000000
+++ /dev/null
@@ -1,60 +0,0 @@
-<?php
-/*
- * Copyright 2019 Wikimedia 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.
- */
-
-/**
- * @group Media
- * @covers MSCompoundFileReader
- */
-class MSCompoundFileReaderTest extends PHPUnit\Framework\TestCase {
-       public static function provideValid() {
-               return [
-                       [ 'calc.xls', 'application/vnd.ms-excel' ],
-                       [ 'excel2016-compat97.xls', 'application/vnd.ms-excel' ],
-                       [ 'gnumeric.xls', 'application/vnd.ms-excel' ],
-                       [ 'impress.ppt', 'application/vnd.ms-powerpoint' ],
-                       [ 'powerpoint2016-compat97.ppt', 'application/vnd.ms-powerpoint' ],
-                       [ 'word2016-compat97.doc', 'application/msword' ],
-                       [ 'writer.doc', 'application/msword' ],
-               ];
-       }
-
-       /** @dataProvider provideValid */
-       public function testReadFile( $fileName, $expectedMime ) {
-               global $IP;
-
-               $info = MSCompoundFileReader::readFile( "$IP/tests/phpunit/data/MSCompoundFileReader/$fileName" );
-               $this->assertTrue( $info['valid'] );
-               $this->assertSame( $expectedMime, $info['mime'] );
-       }
-
-       public static function provideInvalid() {
-               return [
-                       [ 'dir-beyond-end.xls', 'ERROR_READ_PAST_END' ],
-                       [ 'fat-loop.xls', 'ERROR_INVALID_FORMAT' ],
-                       [ 'invalid-signature.xls', 'ERROR_INVALID_SIGNATURE' ],
-               ];
-       }
-
-       /** @dataProvider provideInvalid */
-       public function testReadFileInvalid( $fileName, $expectedError ) {
-               global $IP;
-
-               $info = MSCompoundFileReader::readFile( "$IP/tests/phpunit/data/MSCompoundFileReader/$fileName" );
-               $this->assertFalse( $info['valid'] );
-               $this->assertSame( constant( MSCompoundFileReader::class . '::' . $expectedError ),
-                       $info['errorCode'] );
-       }
-}
diff --git a/tests/phpunit/includes/libs/mime/MimeAnalyzerTest.php b/tests/phpunit/includes/libs/mime/MimeAnalyzerTest.php
deleted file mode 100644 (file)
index 1947812..0000000
+++ /dev/null
@@ -1,140 +0,0 @@
-<?php
-/**
- * @group Media
- * @covers MimeAnalyzer
- */
-class MimeAnalyzerTest extends PHPUnit\Framework\TestCase {
-
-       use MediaWikiCoversValidator;
-
-       /** @var MimeAnalyzer */
-       private $mimeAnalyzer;
-
-       function setUp() {
-               global $IP;
-
-               $this->mimeAnalyzer = new MimeAnalyzer( [
-                       'infoFile' => $IP . "/includes/libs/mime/mime.info",
-                       'typeFile' => $IP . "/includes/libs/mime/mime.types",
-                       'xmlTypes' => [
-                               'http://www.w3.org/2000/svg:svg' => 'image/svg+xml',
-                               'svg' => 'image/svg+xml',
-                               'http://www.lysator.liu.se/~alla/dia/:diagram' => 'application/x-dia-diagram',
-                               'http://www.w3.org/1999/xhtml:html' => 'text/html', // application/xhtml+xml?
-                               'html' => 'text/html', // application/xhtml+xml?
-                       ]
-               ] );
-               parent::setUp();
-       }
-
-       function doGuessMimeType( array $parameters = [] ) {
-               $class = new ReflectionClass( get_class( $this->mimeAnalyzer ) );
-               $method = $class->getMethod( 'doGuessMimeType' );
-               $method->setAccessible( true );
-               return $method->invokeArgs( $this->mimeAnalyzer, $parameters );
-       }
-
-       /**
-        * @dataProvider providerImproveTypeFromExtension
-        * @param string $ext File extension (no leading dot)
-        * @param string $oldMime Initially detected MIME
-        * @param string $expectedMime MIME type after taking extension into account
-        */
-       function testImproveTypeFromExtension( $ext, $oldMime, $expectedMime ) {
-               $actualMime = $this->mimeAnalyzer->improveTypeFromExtension( $oldMime, $ext );
-               $this->assertEquals( $expectedMime, $actualMime );
-       }
-
-       function providerImproveTypeFromExtension() {
-               return [
-                       [ 'gif', 'image/gif', 'image/gif' ],
-                       [ 'gif', 'unknown/unknown', 'unknown/unknown' ],
-                       [ 'wrl', 'unknown/unknown', 'model/vrml' ],
-                       [ 'txt', 'text/plain', 'text/plain' ],
-                       [ 'csv', 'text/plain', 'text/csv' ],
-                       [ 'tsv', 'text/plain', 'text/tab-separated-values' ],
-                       [ 'js', 'text/javascript', 'application/javascript' ],
-                       [ 'js', 'application/x-javascript', 'application/javascript' ],
-                       [ 'json', 'text/plain', 'application/json' ],
-                       [ 'foo', 'application/x-opc+zip', 'application/zip' ],
-                       [ 'docx', 'application/x-opc+zip',
-                               'application/vnd.openxmlformats-officedocument.wordprocessingml.document' ],
-                       [ 'djvu', 'image/x-djvu', 'image/vnd.djvu' ],
-                       [ 'wav', 'audio/wav', 'audio/wav' ],
-               ];
-       }
-
-       /**
-        * Test to make sure that encoder=ffmpeg2theora doesn't trigger
-        * MEDIATYPE_VIDEO (T65584)
-        */
-       function testOggRecognize() {
-               $oggFile = __DIR__ . '/../../../data/media/say-test.ogg';
-               $actualType = $this->mimeAnalyzer->getMediaType( $oggFile, 'application/ogg' );
-               $this->assertEquals( MEDIATYPE_AUDIO, $actualType );
-       }
-
-       /**
-        * Test to make sure that Opus audio files don't trigger
-        * MEDIATYPE_MULTIMEDIA (bug T151352)
-        */
-       function testOpusRecognize() {
-               $oggFile = __DIR__ . '/../../../data/media/say-test.opus';
-               $actualType = $this->mimeAnalyzer->getMediaType( $oggFile, 'application/ogg' );
-               $this->assertEquals( MEDIATYPE_AUDIO, $actualType );
-       }
-
-       /**
-        * Test to make sure that mp3 files are detected as audio type
-        */
-       function testMP3AsAudio() {
-               $file = __DIR__ . '/../../../data/media/say-test-with-id3.mp3';
-               $actualType = $this->mimeAnalyzer->getMediaType( $file );
-               $this->assertEquals( MEDIATYPE_AUDIO, $actualType );
-       }
-
-       /**
-        * Test to make sure that MP3 with id3 tag is recognized
-        */
-       function testMP3WithID3Recognize() {
-               $file = __DIR__ . '/../../../data/media/say-test-with-id3.mp3';
-               $actualType = $this->doGuessMimeType( [ $file, 'mp3' ] );
-               $this->assertEquals( 'audio/mpeg', $actualType );
-       }
-
-       /**
-        * Test to make sure that MP3 without id3 tag is recognized (MPEG-1 sample rates)
-        */
-       function testMP3NoID3RecognizeMPEG1() {
-               $file = __DIR__ . '/../../../data/media/say-test-mpeg1.mp3';
-               $actualType = $this->doGuessMimeType( [ $file, 'mp3' ] );
-               $this->assertEquals( 'audio/mpeg', $actualType );
-       }
-
-       /**
-        * Test to make sure that MP3 without id3 tag is recognized (MPEG-2 sample rates)
-        */
-       function testMP3NoID3RecognizeMPEG2() {
-               $file = __DIR__ . '/../../../data/media/say-test-mpeg2.mp3';
-               $actualType = $this->doGuessMimeType( [ $file, 'mp3' ] );
-               $this->assertEquals( 'audio/mpeg', $actualType );
-       }
-
-       /**
-        * Test to make sure that MP3 without id3 tag is recognized (MPEG-2.5 sample rates)
-        */
-       function testMP3NoID3RecognizeMPEG2_5() {
-               $file = __DIR__ . '/../../../data/media/say-test-mpeg2.5.mp3';
-               $actualType = $this->doGuessMimeType( [ $file, 'mp3' ] );
-               $this->assertEquals( 'audio/mpeg', $actualType );
-       }
-
-       /**
-        * A ZIP file embedded in the middle of a .doc file is still a Word Document.
-        */
-       function testZipInDoc() {
-               $file = __DIR__ . '/../../../data/media/zip-in-doc.doc';
-               $actualType = $this->doGuessMimeType( [ $file, 'doc' ] );
-               $this->assertEquals( 'application/msword', $actualType );
-       }
-}
diff --git a/tests/phpunit/includes/libs/objectcache/CachedBagOStuffTest.php b/tests/phpunit/includes/libs/objectcache/CachedBagOStuffTest.php
deleted file mode 100644 (file)
index f953319..0000000
+++ /dev/null
@@ -1,159 +0,0 @@
-<?php
-
-use Wikimedia\TestingAccessWrapper;
-
-/**
- * @group BagOStuff
- */
-class CachedBagOStuffTest extends PHPUnit\Framework\TestCase {
-
-       use MediaWikiCoversValidator;
-
-       /**
-        * @covers CachedBagOStuff::__construct
-        * @covers CachedBagOStuff::get
-        */
-       public function testGetFromBackend() {
-               $backend = new HashBagOStuff;
-               $cache = new CachedBagOStuff( $backend );
-
-               $backend->set( 'foo', 'bar' );
-               $this->assertEquals( 'bar', $cache->get( 'foo' ) );
-
-               $backend->set( 'foo', 'baz' );
-               $this->assertEquals( 'bar', $cache->get( 'foo' ), 'cached' );
-       }
-
-       /**
-        * @covers CachedBagOStuff::set
-        * @covers CachedBagOStuff::delete
-        */
-       public function testSetAndDelete() {
-               $backend = new HashBagOStuff;
-               $cache = new CachedBagOStuff( $backend );
-
-               for ( $i = 0; $i < 10; $i++ ) {
-                       $cache->set( "key$i", 1 );
-                       $this->assertEquals( 1, $cache->get( "key$i" ) );
-                       $this->assertEquals( 1, $backend->get( "key$i" ) );
-
-                       $cache->delete( "key$i" );
-                       $this->assertEquals( false, $cache->get( "key$i" ) );
-                       $this->assertEquals( false, $backend->get( "key$i" ) );
-               }
-       }
-
-       /**
-        * @covers CachedBagOStuff::set
-        * @covers CachedBagOStuff::delete
-        */
-       public function testWriteCacheOnly() {
-               $backend = new HashBagOStuff;
-               $cache = new CachedBagOStuff( $backend );
-
-               $cache->set( 'foo', 'bar', 0, CachedBagOStuff::WRITE_CACHE_ONLY );
-               $this->assertEquals( 'bar', $cache->get( 'foo' ) );
-               $this->assertFalse( $backend->get( 'foo' ) );
-
-               $cache->set( 'foo', 'old' );
-               $this->assertEquals( 'old', $cache->get( 'foo' ) );
-               $this->assertEquals( 'old', $backend->get( 'foo' ) );
-
-               $cache->set( 'foo', 'new', 0, CachedBagOStuff::WRITE_CACHE_ONLY );
-               $this->assertEquals( 'new', $cache->get( 'foo' ) );
-               $this->assertEquals( 'old', $backend->get( 'foo' ) );
-
-               $cache->delete( 'foo', CachedBagOStuff::WRITE_CACHE_ONLY );
-               $this->assertEquals( 'old', $cache->get( 'foo' ) ); // Reloaded from backend
-       }
-
-       /**
-        * @covers CachedBagOStuff::get
-        */
-       public function testCacheBackendMisses() {
-               $backend = new HashBagOStuff;
-               $cache = new CachedBagOStuff( $backend );
-
-               // First hit primes the cache with miss from the backend
-               $this->assertEquals( false, $cache->get( 'foo' ) );
-
-               // Change the value in the backend
-               $backend->set( 'foo', true );
-
-               // Second hit returns the cached miss
-               $this->assertEquals( false, $cache->get( 'foo' ) );
-
-               // But a fresh value is read from the backend
-               $backend->set( 'bar', true );
-               $this->assertEquals( true, $cache->get( 'bar' ) );
-       }
-
-       /**
-        * @covers CachedBagOStuff::setDebug
-        */
-       public function testSetDebug() {
-               $backend = new HashBagOStuff();
-               $cache = new CachedBagOStuff( $backend );
-               // Access private property 'debugMode'
-               $backend = TestingAccessWrapper::newFromObject( $backend );
-               $cache = TestingAccessWrapper::newFromObject( $cache );
-               $this->assertFalse( $backend->debugMode );
-               $this->assertFalse( $cache->debugMode );
-
-               $cache->setDebug( true );
-               // Should have set both
-               $this->assertTrue( $backend->debugMode, 'sets backend' );
-               $this->assertTrue( $cache->debugMode, 'sets self' );
-       }
-
-       /**
-        * @covers CachedBagOStuff::deleteObjectsExpiringBefore
-        */
-       public function testExpire() {
-               $backend = $this->getMockBuilder( HashBagOStuff::class )
-                       ->setMethods( [ 'deleteObjectsExpiringBefore' ] )
-                       ->getMock();
-               $backend->expects( $this->once() )
-                       ->method( 'deleteObjectsExpiringBefore' )
-                       ->willReturn( false );
-
-               $cache = new CachedBagOStuff( $backend );
-               $cache->deleteObjectsExpiringBefore( '20110401000000' );
-       }
-
-       /**
-        * @covers CachedBagOStuff::makeKey
-        */
-       public function testMakeKey() {
-               $backend = $this->getMockBuilder( HashBagOStuff::class )
-                       ->setMethods( [ 'makeKey' ] )
-                       ->getMock();
-               $backend->method( 'makeKey' )
-                       ->willReturn( 'special/logic' );
-
-               // CachedBagOStuff wraps any backend with a process cache
-               // using HashBagOStuff. Hash has no special key limitations,
-               // but backends often do. Make sure it uses the backend's
-               // makeKey() logic, not the one inherited from HashBagOStuff
-               $cache = new CachedBagOStuff( $backend );
-
-               $this->assertEquals( 'special/logic', $backend->makeKey( 'special', 'logic' ) );
-               $this->assertEquals( 'special/logic', $cache->makeKey( 'special', 'logic' ) );
-       }
-
-       /**
-        * @covers CachedBagOStuff::makeGlobalKey
-        */
-       public function testMakeGlobalKey() {
-               $backend = $this->getMockBuilder( HashBagOStuff::class )
-                       ->setMethods( [ 'makeGlobalKey' ] )
-                       ->getMock();
-               $backend->method( 'makeGlobalKey' )
-                       ->willReturn( 'special/logic' );
-
-               $cache = new CachedBagOStuff( $backend );
-
-               $this->assertEquals( 'special/logic', $backend->makeGlobalKey( 'special', 'logic' ) );
-               $this->assertEquals( 'special/logic', $cache->makeGlobalKey( 'special', 'logic' ) );
-       }
-}
diff --git a/tests/phpunit/includes/libs/objectcache/HashBagOStuffTest.php b/tests/phpunit/includes/libs/objectcache/HashBagOStuffTest.php
deleted file mode 100644 (file)
index 332e23b..0000000
+++ /dev/null
@@ -1,163 +0,0 @@
-<?php
-
-use Wikimedia\TestingAccessWrapper;
-
-/**
- * @group BagOStuff
- */
-class HashBagOStuffTest extends PHPUnit\Framework\TestCase {
-
-       use MediaWikiCoversValidator;
-
-       /**
-        * @covers HashBagOStuff::__construct
-        */
-       public function testConstruct() {
-               $this->assertInstanceOf(
-                       HashBagOStuff::class,
-                       new HashBagOStuff()
-               );
-       }
-
-       /**
-        * @covers HashBagOStuff::__construct
-        * @expectedException InvalidArgumentException
-        */
-       public function testConstructBadZero() {
-               $cache = new HashBagOStuff( [ 'maxKeys' => 0 ] );
-       }
-
-       /**
-        * @covers HashBagOStuff::__construct
-        * @expectedException InvalidArgumentException
-        */
-       public function testConstructBadNeg() {
-               $cache = new HashBagOStuff( [ 'maxKeys' => -1 ] );
-       }
-
-       /**
-        * @covers HashBagOStuff::__construct
-        * @expectedException InvalidArgumentException
-        */
-       public function testConstructBadType() {
-               $cache = new HashBagOStuff( [ 'maxKeys' => 'x' ] );
-       }
-
-       /**
-        * @covers HashBagOStuff::delete
-        */
-       public function testDelete() {
-               $cache = new HashBagOStuff();
-               for ( $i = 0; $i < 10; $i++ ) {
-                       $cache->set( "key$i", 1 );
-                       $this->assertEquals( 1, $cache->get( "key$i" ) );
-                       $cache->delete( "key$i" );
-                       $this->assertEquals( false, $cache->get( "key$i" ) );
-               }
-       }
-
-       /**
-        * @covers HashBagOStuff::clear
-        */
-       public function testClear() {
-               $cache = new HashBagOStuff();
-               for ( $i = 0; $i < 10; $i++ ) {
-                       $cache->set( "key$i", 1 );
-                       $this->assertEquals( 1, $cache->get( "key$i" ) );
-               }
-               $cache->clear();
-               for ( $i = 0; $i < 10; $i++ ) {
-                       $this->assertEquals( false, $cache->get( "key$i" ) );
-               }
-       }
-
-       /**
-        * @covers HashBagOStuff::doGet
-        * @covers HashBagOStuff::expire
-        */
-       public function testExpire() {
-               $cache = new HashBagOStuff();
-               $cacheInternal = TestingAccessWrapper::newFromObject( $cache );
-               $cache->set( 'foo', 1 );
-               $cache->set( 'bar', 1, 10 );
-               $cache->set( 'baz', 1, -10 );
-
-               $this->assertEquals( 0, $cacheInternal->bag['foo'][$cache::KEY_EXP], 'Indefinite' );
-               // 2 seconds tolerance
-               $this->assertEquals( time() + 10, $cacheInternal->bag['bar'][$cache::KEY_EXP], 'Future', 2 );
-               $this->assertEquals( time() - 10, $cacheInternal->bag['baz'][$cache::KEY_EXP], 'Past', 2 );
-
-               $this->assertEquals( 1, $cache->get( 'bar' ), 'Key not expired' );
-               $this->assertEquals( false, $cache->get( 'baz' ), 'Key expired' );
-       }
-
-       /**
-        * Ensure maxKeys eviction prefers keeping new keys.
-        *
-        * @covers HashBagOStuff::set
-        */
-       public function testEvictionAdd() {
-               $cache = new HashBagOStuff( [ 'maxKeys' => 10 ] );
-               for ( $i = 0; $i < 10; $i++ ) {
-                       $cache->set( "key$i", 1 );
-                       $this->assertEquals( 1, $cache->get( "key$i" ) );
-               }
-               for ( $i = 10; $i < 20; $i++ ) {
-                       $cache->set( "key$i", 1 );
-                       $this->assertEquals( 1, $cache->get( "key$i" ) );
-                       $this->assertEquals( false, $cache->get( "key" . ( $i - 10 ) ) );
-               }
-       }
-
-       /**
-        * Ensure maxKeys eviction prefers recently set keys
-        * even if the keys pre-exist.
-        *
-        * @covers HashBagOStuff::set
-        */
-       public function testEvictionSet() {
-               $cache = new HashBagOStuff( [ 'maxKeys' => 3 ] );
-
-               foreach ( [ 'foo', 'bar', 'baz' ] as $key ) {
-                       $cache->set( $key, 1 );
-               }
-
-               // Set existing key
-               $cache->set( 'foo', 1 );
-
-               // Add a 4th key (beyond the allowed maximum)
-               $cache->set( 'quux', 1 );
-
-               // Foo's life should have been extended over Bar
-               foreach ( [ 'foo', 'baz', 'quux' ] as $key ) {
-                       $this->assertEquals( 1, $cache->get( $key ), "Kept $key" );
-               }
-               $this->assertEquals( false, $cache->get( 'bar' ), 'Evicted bar' );
-       }
-
-       /**
-        * Ensure maxKeys eviction prefers recently retrieved keys (LRU).
-        *
-        * @covers HashBagOStuff::doGet
-        * @covers HashBagOStuff::hasKey
-        */
-       public function testEvictionGet() {
-               $cache = new HashBagOStuff( [ 'maxKeys' => 3 ] );
-
-               foreach ( [ 'foo', 'bar', 'baz' ] as $key ) {
-                       $cache->set( $key, 1 );
-               }
-
-               // Get existing key
-               $cache->get( 'foo', 1 );
-
-               // Add a 4th key (beyond the allowed maximum)
-               $cache->set( 'quux', 1 );
-
-               // Foo's life should have been extended over Bar
-               foreach ( [ 'foo', 'baz', 'quux' ] as $key ) {
-                       $this->assertEquals( 1, $cache->get( $key ), "Kept $key" );
-               }
-               $this->assertEquals( false, $cache->get( 'bar' ), 'Evicted bar' );
-       }
-}
diff --git a/tests/phpunit/includes/libs/objectcache/ReplicatedBagOStuffTest.php b/tests/phpunit/includes/libs/objectcache/ReplicatedBagOStuffTest.php
deleted file mode 100644 (file)
index 550ec0b..0000000
+++ /dev/null
@@ -1,62 +0,0 @@
-<?php
-
-class ReplicatedBagOStuffTest extends MediaWikiTestCase {
-       /** @var HashBagOStuff */
-       private $writeCache;
-       /** @var HashBagOStuff */
-       private $readCache;
-       /** @var ReplicatedBagOStuff */
-       private $cache;
-
-       protected function setUp() {
-               parent::setUp();
-
-               $this->writeCache = new HashBagOStuff();
-               $this->readCache = new HashBagOStuff();
-               $this->cache = new ReplicatedBagOStuff( [
-                       'writeFactory' => $this->writeCache,
-                       'readFactory' => $this->readCache,
-               ] );
-       }
-
-       /**
-        * @covers ReplicatedBagOStuff::set
-        */
-       public function testSet() {
-               $key = 'a key';
-               $value = 'a value';
-               $this->cache->set( $key, $value );
-
-               // Write to master.
-               $this->assertEquals( $value, $this->writeCache->get( $key ) );
-               // Don't write to replica. Replication is deferred to backend.
-               $this->assertFalse( $this->readCache->get( $key ) );
-       }
-
-       /**
-        * @covers ReplicatedBagOStuff::get
-        */
-       public function testGet() {
-               $key = 'a key';
-
-               $write = 'one value';
-               $this->writeCache->set( $key, $write );
-               $read = 'another value';
-               $this->readCache->set( $key, $read );
-
-               // Read from replica.
-               $this->assertEquals( $read, $this->cache->get( $key ) );
-       }
-
-       /**
-        * @covers ReplicatedBagOStuff::get
-        */
-       public function testGetAbsent() {
-               $key = 'a key';
-               $value = 'a value';
-               $this->writeCache->set( $key, $value );
-
-               // Don't read from master. No failover if value is absent.
-               $this->assertFalse( $this->cache->get( $key ) );
-       }
-}
diff --git a/tests/phpunit/includes/libs/objectcache/WANObjectCacheTest.php b/tests/phpunit/includes/libs/objectcache/WANObjectCacheTest.php
deleted file mode 100644 (file)
index 017d745..0000000
+++ /dev/null
@@ -1,1867 +0,0 @@
-<?php
-
-use Wikimedia\TestingAccessWrapper;
-
-/**
- * @covers WANObjectCache::wrap
- * @covers WANObjectCache::unwrap
- * @covers WANObjectCache::worthRefreshExpiring
- * @covers WANObjectCache::worthRefreshPopular
- * @covers WANObjectCache::isValid
- * @covers WANObjectCache::getWarmupKeyMisses
- * @covers WANObjectCache::prefixCacheKeys
- * @covers WANObjectCache::getProcessCache
- * @covers WANObjectCache::getNonProcessCachedKeys
- * @covers WANObjectCache::getRawKeysForWarmup
- * @covers WANObjectCache::getInterimValue
- * @covers WANObjectCache::setInterimValue
- */
-class WANObjectCacheTest extends PHPUnit\Framework\TestCase {
-
-       use MediaWikiCoversValidator;
-       use PHPUnit4And6Compat;
-
-       /** @var WANObjectCache */
-       private $cache;
-       /** @var BagOStuff */
-       private $internalCache;
-
-       protected function setUp() {
-               parent::setUp();
-
-               $this->cache = new WANObjectCache( [
-                       'cache' => new HashBagOStuff()
-               ] );
-
-               $wanCache = TestingAccessWrapper::newFromObject( $this->cache );
-               /** @noinspection PhpUndefinedFieldInspection */
-               $this->internalCache = $wanCache->cache;
-       }
-
-       /**
-        * @dataProvider provideSetAndGet
-        * @covers WANObjectCache::set()
-        * @covers WANObjectCache::get()
-        * @covers WANObjectCache::makeKey()
-        * @param mixed $value
-        * @param int $ttl
-        */
-       public function testSetAndGet( $value, $ttl ) {
-               $curTTL = null;
-               $asOf = null;
-               $key = $this->cache->makeKey( 'x', wfRandomString() );
-
-               $this->cache->get( $key, $curTTL, [], $asOf );
-               $this->assertNull( $curTTL, "Current TTL is null" );
-               $this->assertNull( $asOf, "Current as-of-time is infinite" );
-
-               $t = microtime( true );
-               $this->cache->set( $key, $value, $ttl );
-
-               $this->assertEquals( $value, $this->cache->get( $key, $curTTL, [], $asOf ) );
-               if ( is_infinite( $ttl ) || $ttl == 0 ) {
-                       $this->assertTrue( is_infinite( $curTTL ), "Current TTL is infinite" );
-               } else {
-                       $this->assertGreaterThan( 0, $curTTL, "Current TTL > 0" );
-                       $this->assertLessThanOrEqual( $ttl, $curTTL, "Current TTL < nominal TTL" );
-               }
-               $this->assertGreaterThanOrEqual( $t - 1, $asOf, "As-of-time in range of set() time" );
-               $this->assertLessThanOrEqual( $t + 1, $asOf, "As-of-time in range of set() time" );
-       }
-
-       public static function provideSetAndGet() {
-               return [
-                       [ 14141, 3 ],
-                       [ 3535.666, 3 ],
-                       [ [], 3 ],
-                       [ null, 3 ],
-                       [ '0', 3 ],
-                       [ (object)[ 'meow' ], 3 ],
-                       [ INF, 3 ],
-                       [ '', 3 ],
-                       [ 'pizzacat', INF ],
-               ];
-       }
-
-       /**
-        * @covers WANObjectCache::get()
-        * @covers WANObjectCache::makeGlobalKey()
-        */
-       public function testGetNotExists() {
-               $key = $this->cache->makeGlobalKey( 'y', wfRandomString(), 'p' );
-               $curTTL = null;
-               $value = $this->cache->get( $key, $curTTL );
-
-               $this->assertFalse( $value, "Non-existing key has false value" );
-               $this->assertNull( $curTTL, "Non-existing key has null current TTL" );
-       }
-
-       /**
-        * @covers WANObjectCache::set()
-        */
-       public function testSetOver() {
-               $key = wfRandomString();
-               for ( $i = 0; $i < 3; ++$i ) {
-                       $value = wfRandomString();
-                       $this->cache->set( $key, $value, 3 );
-
-                       $this->assertEquals( $this->cache->get( $key ), $value );
-               }
-       }
-
-       /**
-        * @covers WANObjectCache::set()
-        */
-       public function testStaleSet() {
-               $key = wfRandomString();
-               $value = wfRandomString();
-               $this->cache->set( $key, $value, 3, [ 'since' => microtime( true ) - 30 ] );
-
-               $this->assertFalse( $this->cache->get( $key ), "Stale set() value ignored" );
-       }
-
-       public function testProcessCache() {
-               $mockWallClock = 1549343530.2053;
-               $this->cache->setMockTime( $mockWallClock );
-
-               $hit = 0;
-               $callback = function () use ( &$hit ) {
-                       ++$hit;
-                       return 42;
-               };
-               $keys = [ wfRandomString(), wfRandomString(), wfRandomString() ];
-               $groups = [ 'thiscache:1', 'thatcache:1', 'somecache:1' ];
-
-               foreach ( $keys as $i => $key ) {
-                       $this->cache->getWithSetCallback(
-                               $key, 100, $callback, [ 'pcTTL' => 5, 'pcGroup' => $groups[$i] ] );
-               }
-               $this->assertEquals( 3, $hit );
-
-               foreach ( $keys as $i => $key ) {
-                       $this->cache->getWithSetCallback(
-                               $key, 100, $callback, [ 'pcTTL' => 5, 'pcGroup' => $groups[$i] ] );
-               }
-               $this->assertEquals( 3, $hit, "Values cached" );
-
-               foreach ( $keys as $i => $key ) {
-                       $this->cache->getWithSetCallback(
-                               "$key-2", 100, $callback, [ 'pcTTL' => 5, 'pcGroup' => $groups[$i] ] );
-               }
-               $this->assertEquals( 6, $hit );
-
-               foreach ( $keys as $i => $key ) {
-                       $this->cache->getWithSetCallback(
-                               "$key-2", 100, $callback, [ 'pcTTL' => 5, 'pcGroup' => $groups[$i] ] );
-               }
-               $this->assertEquals( 6, $hit, "New values cached" );
-
-               foreach ( $keys as $i => $key ) {
-                       // Should evict from process cache
-                       $this->cache->delete( $key );
-                       $mockWallClock += 0.001; // cached values will be newer than tombstone
-                       // Get into cache (specific process cache group)
-                       $this->cache->getWithSetCallback(
-                               $key, 100, $callback, [ 'pcTTL' => 5, 'pcGroup' => $groups[$i] ] );
-               }
-               $this->assertEquals( 9, $hit, "Values evicted by delete()" );
-
-               // Get into cache (default process cache group)
-               $key = reset( $keys );
-               $this->cache->getWithSetCallback( $key, 100, $callback, [ 'pcTTL' => 5 ] );
-               $this->assertEquals( 9, $hit, "Value recently interim-cached" );
-
-               $mockWallClock += 0.2; // interim key not brand new
-               $this->cache->clearProcessCache();
-               $this->cache->getWithSetCallback( $key, 100, $callback, [ 'pcTTL' => 5 ] );
-               $this->assertEquals( 10, $hit, "Value calculated (interim key not recent and reset)" );
-               $this->cache->getWithSetCallback( $key, 100, $callback, [ 'pcTTL' => 5 ] );
-               $this->assertEquals( 10, $hit, "Value process cached" );
-
-               $mockWallClock += 0.2; // interim key not brand new
-               $outerCallback = function () use ( &$callback, $key ) {
-                       $v = $this->cache->getWithSetCallback( $key, 100, $callback, [ 'pcTTL' => 5 ] );
-
-                       return 43 + $v;
-               };
-               // Outer key misses and refuses inner key process cache value
-               $this->cache->getWithSetCallback( "$key-miss-outer", 100, $outerCallback );
-               $this->assertEquals( 11, $hit, "Nested callback value process cache skipped" );
-       }
-
-       /**
-        * @dataProvider getWithSetCallback_provider
-        * @covers WANObjectCache::getWithSetCallback()
-        * @covers WANObjectCache::doGetWithSetCallback()
-        * @param array $extOpts
-        * @param bool $versioned
-        */
-       public function testGetWithSetCallback( array $extOpts, $versioned ) {
-               $cache = $this->cache;
-
-               $key = wfRandomString();
-               $value = wfRandomString();
-               $cKey1 = wfRandomString();
-               $cKey2 = wfRandomString();
-
-               $priorValue = null;
-               $priorAsOf = null;
-               $wasSet = 0;
-               $func = function ( $old, &$ttl, &$opts, $asOf )
-               use ( &$wasSet, &$priorValue, &$priorAsOf, $value ) {
-                       ++$wasSet;
-                       $priorValue = $old;
-                       $priorAsOf = $asOf;
-                       $ttl = 20; // override with another value
-                       return $value;
-               };
-
-               $mockWallClock = 1549343530.2053;
-               $priorTime = $mockWallClock; // reference time
-               $cache->setMockTime( $mockWallClock );
-
-               $wasSet = 0;
-               $v = $cache->getWithSetCallback( $key, 30, $func, [ 'lockTSE' => 5 ] + $extOpts );
-               $this->assertEquals( $value, $v, "Value returned" );
-               $this->assertEquals( 1, $wasSet, "Value regenerated" );
-               $this->assertFalse( $priorValue, "No prior value" );
-               $this->assertNull( $priorAsOf, "No prior value" );
-
-               $curTTL = null;
-               $cache->get( $key, $curTTL );
-               $this->assertLessThanOrEqual( 20, $curTTL, 'Current TTL between 19-20 (overriden)' );
-               $this->assertGreaterThanOrEqual( 19, $curTTL, 'Current TTL between 19-20 (overriden)' );
-
-               $wasSet = 0;
-               $v = $cache->getWithSetCallback(
-                       $key, 30, $func, [ 'lowTTL' => 0, 'lockTSE' => 5 ] + $extOpts );
-               $this->assertEquals( $value, $v, "Value returned" );
-               $this->assertEquals( 0, $wasSet, "Value not regenerated" );
-
-               $mockWallClock += 1;
-
-               $wasSet = 0;
-               $v = $cache->getWithSetCallback(
-                       $key, 30, $func, [ 'checkKeys' => [ $cKey1, $cKey2 ] ] + $extOpts
-               );
-               $this->assertEquals( $value, $v, "Value returned" );
-               $this->assertEquals( 1, $wasSet, "Value regenerated due to check keys" );
-               $this->assertEquals( $value, $priorValue, "Has prior value" );
-               $this->assertInternalType( 'float', $priorAsOf, "Has prior value" );
-               $t1 = $cache->getCheckKeyTime( $cKey1 );
-               $this->assertGreaterThanOrEqual( $priorTime, $t1, 'Check keys generated on miss' );
-               $t2 = $cache->getCheckKeyTime( $cKey2 );
-               $this->assertGreaterThanOrEqual( $priorTime, $t2, 'Check keys generated on miss' );
-
-               $mockWallClock += 0.2; // interim key is not brand new and check keys have past values
-               $priorTime = $mockWallClock; // reference time
-               $wasSet = 0;
-               $v = $cache->getWithSetCallback(
-                       $key, 30, $func, [ 'checkKeys' => [ $cKey1, $cKey2 ] ] + $extOpts
-               );
-               $this->assertEquals( $value, $v, "Value returned" );
-               $this->assertEquals( 1, $wasSet, "Value regenerated due to still-recent check keys" );
-               $t1 = $cache->getCheckKeyTime( $cKey1 );
-               $this->assertLessThanOrEqual( $priorTime, $t1, 'Check keys did not change again' );
-               $t2 = $cache->getCheckKeyTime( $cKey2 );
-               $this->assertLessThanOrEqual( $priorTime, $t2, 'Check keys did not change again' );
-
-               $curTTL = null;
-               $v = $cache->get( $key, $curTTL, [ $cKey1, $cKey2 ] );
-               if ( $versioned ) {
-                       $this->assertEquals( $value, $v[$cache::VFLD_DATA], "Value returned" );
-               } else {
-                       $this->assertEquals( $value, $v, "Value returned" );
-               }
-               $this->assertLessThanOrEqual( 0, $curTTL, "Value has current TTL < 0 due to check keys" );
-
-               $wasSet = 0;
-               $key = wfRandomString();
-               $v = $cache->getWithSetCallback( $key, 30, $func, [ 'pcTTL' => 5 ] + $extOpts );
-               $this->assertEquals( $value, $v, "Value returned" );
-               $cache->delete( $key );
-               $v = $cache->getWithSetCallback( $key, 30, $func, [ 'pcTTL' => 5 ] + $extOpts );
-               $this->assertEquals( $value, $v, "Value still returned after deleted" );
-               $this->assertEquals( 1, $wasSet, "Value process cached while deleted" );
-
-               $oldValReceived = -1;
-               $oldAsOfReceived = -1;
-               $checkFunc = function ( $oldVal, &$ttl, array $setOpts, $oldAsOf )
-               use ( &$oldValReceived, &$oldAsOfReceived, &$wasSet ) {
-                       ++$wasSet;
-                       $oldValReceived = $oldVal;
-                       $oldAsOfReceived = $oldAsOf;
-
-                       return 'xxx' . $wasSet;
-               };
-
-               $mockWallClock = 1549343530.2053;
-               $priorTime = $mockWallClock; // reference time
-
-               $wasSet = 0;
-               $key = wfRandomString();
-               $v = $cache->getWithSetCallback(
-                       $key, 30, $checkFunc, [ 'staleTTL' => 50 ] + $extOpts );
-               $this->assertEquals( 'xxx1', $v, "Value returned" );
-               $this->assertEquals( false, $oldValReceived, "Callback got no stale value" );
-               $this->assertEquals( null, $oldAsOfReceived, "Callback got no stale value" );
-
-               $mockWallClock += 40;
-               $v = $cache->getWithSetCallback(
-                       $key, 30, $checkFunc, [ 'staleTTL' => 50 ] + $extOpts );
-               $this->assertEquals( 'xxx2', $v, "Value still returned after expired" );
-               $this->assertEquals( 2, $wasSet, "Value recalculated while expired" );
-               $this->assertEquals( 'xxx1', $oldValReceived, "Callback got stale value" );
-               $this->assertNotEquals( null, $oldAsOfReceived, "Callback got stale value" );
-
-               $mockWallClock += 260;
-               $v = $cache->getWithSetCallback(
-                       $key, 30, $checkFunc, [ 'staleTTL' => 50 ] + $extOpts );
-               $this->assertEquals( 'xxx3', $v, "Value still returned after expired" );
-               $this->assertEquals( 3, $wasSet, "Value recalculated while expired" );
-               $this->assertEquals( false, $oldValReceived, "Callback got no stale value" );
-               $this->assertEquals( null, $oldAsOfReceived, "Callback got no stale value" );
-
-               $mockWallClock = ( $priorTime - $cache::HOLDOFF_TTL - 1 );
-               $wasSet = 0;
-               $key = wfRandomString();
-               $checkKey = $cache->makeKey( 'template', 'X' );
-               $cache->touchCheckKey( $checkKey ); // init check key
-               $mockWallClock = $priorTime;
-               $v = $cache->getWithSetCallback(
-                       $key,
-                       $cache::TTL_INDEFINITE,
-                       $checkFunc,
-                       [ 'graceTTL' => $cache::TTL_WEEK, 'checkKeys' => [ $checkKey ] ] + $extOpts
-               );
-               $this->assertEquals( 'xxx1', $v, "Value returned" );
-               $this->assertEquals( 1, $wasSet, "Value computed" );
-               $this->assertEquals( false, $oldValReceived, "Callback got no stale value" );
-               $this->assertEquals( null, $oldAsOfReceived, "Callback got no stale value" );
-
-               $mockWallClock += $cache::TTL_HOUR; // some time passes
-               $v = $cache->getWithSetCallback(
-                       $key,
-                       $cache::TTL_INDEFINITE,
-                       $checkFunc,
-                       [ 'graceTTL' => $cache::TTL_WEEK, 'checkKeys' => [ $checkKey ] ] + $extOpts
-               );
-               $this->assertEquals( 'xxx1', $v, "Cached value returned" );
-               $this->assertEquals( 1, $wasSet, "Cached value returned" );
-
-               $cache->touchCheckKey( $checkKey ); // make key stale
-               $mockWallClock += 0.01; // ~1 week left of grace (barely stale to avoid refreshes)
-
-               $v = $cache->getWithSetCallback(
-                       $key,
-                       $cache::TTL_INDEFINITE,
-                       $checkFunc,
-                       [ 'graceTTL' => $cache::TTL_WEEK, 'checkKeys' => [ $checkKey ] ] + $extOpts
-               );
-               $this->assertEquals( 'xxx1', $v, "Value still returned after expired (in grace)" );
-               $this->assertEquals( 1, $wasSet, "Value still returned after expired (in grace)" );
-
-               // Chance of refresh increase to unity as staleness approaches graceTTL
-               $mockWallClock += $cache::TTL_WEEK; // 8 days of being stale
-               $v = $cache->getWithSetCallback(
-                       $key,
-                       $cache::TTL_INDEFINITE,
-                       $checkFunc,
-                       [ 'graceTTL' => $cache::TTL_WEEK, 'checkKeys' => [ $checkKey ] ] + $extOpts
-               );
-               $this->assertEquals( 'xxx2', $v, "Value was recomputed (past grace)" );
-               $this->assertEquals( 2, $wasSet, "Value was recomputed (past grace)" );
-               $this->assertEquals( 'xxx1', $oldValReceived, "Callback got post-grace stale value" );
-               $this->assertNotEquals( null, $oldAsOfReceived, "Callback got post-grace stale value" );
-       }
-
-       /**
-        * @dataProvider getWithSetCallback_provider
-        * @covers WANObjectCache::getWithSetCallback()
-        * @covers WANObjectCache::doGetWithSetCallback()
-        * @param array $extOpts
-        * @param bool $versioned
-        */
-       function testGetWithSetcallback_touched( array $extOpts, $versioned ) {
-               $cache = $this->cache;
-
-               $mockWallClock = 1549343530.2053;
-               $cache->setMockTime( $mockWallClock );
-
-               $checkFunc = function ( $oldVal, &$ttl, array $setOpts, $oldAsOf )
-               use ( &$wasSet ) {
-                       ++$wasSet;
-
-                       return 'xxx' . $wasSet;
-               };
-
-               $key = wfRandomString();
-               $wasSet = 0;
-               $touched = null;
-               $touchedCallback = function () use ( &$touched ) {
-                       return $touched;
-               };
-               $v = $cache->getWithSetCallback(
-                       $key,
-                       $cache::TTL_INDEFINITE,
-                       $checkFunc,
-                       [ 'touchedCallback' => $touchedCallback ] + $extOpts
-               );
-               $mockWallClock += 60;
-               $v = $cache->getWithSetCallback(
-                       $key,
-                       $cache::TTL_INDEFINITE,
-                       $checkFunc,
-                       [ 'touchedCallback' => $touchedCallback ] + $extOpts
-               );
-               $this->assertEquals( 'xxx1', $v, "Value was computed once" );
-               $this->assertEquals( 1, $wasSet, "Value was computed once" );
-
-               $touched = $mockWallClock - 10;
-               $v = $cache->getWithSetCallback(
-                       $key,
-                       $cache::TTL_INDEFINITE,
-                       $checkFunc,
-                       [ 'touchedCallback' => $touchedCallback ] + $extOpts
-               );
-               $v = $cache->getWithSetCallback(
-                       $key,
-                       $cache::TTL_INDEFINITE,
-                       $checkFunc,
-                       [ 'touchedCallback' => $touchedCallback ] + $extOpts
-               );
-               $this->assertEquals( 'xxx2', $v, "Value was recomputed once" );
-               $this->assertEquals( 2, $wasSet, "Value was recomputed once" );
-       }
-
-       public static function getWithSetCallback_provider() {
-               return [
-                       [ [], false ],
-                       [ [ 'version' => 1 ], true ]
-               ];
-       }
-
-       public function testPreemtiveRefresh() {
-               $value = 'KatCafe';
-               $wasSet = 0;
-               $func = function ( $old, &$ttl, &$opts, $asOf ) use ( &$wasSet, &$value )
-               {
-                       ++$wasSet;
-                       return $value;
-               };
-
-               $cache = new NearExpiringWANObjectCache( [ 'cache' => new HashBagOStuff() ] );
-               $mockWallClock = 1549343530.2053;
-               $cache->setMockTime( $mockWallClock );
-
-               $wasSet = 0;
-               $key = wfRandomString();
-               $opts = [ 'lowTTL' => 30 ];
-               $v = $cache->getWithSetCallback( $key, 20, $func, $opts );
-               $this->assertEquals( $value, $v, "Value returned" );
-               $this->assertEquals( 1, $wasSet, "Value calculated" );
-
-               $mockWallClock += 0.2; // interim key is not brand new
-               $v = $cache->getWithSetCallback( $key, 20, $func, $opts );
-               $this->assertEquals( 2, $wasSet, "Value re-calculated" );
-
-               $wasSet = 0;
-               $key = wfRandomString();
-               $opts = [ 'lowTTL' => 1 ];
-               $v = $cache->getWithSetCallback( $key, 30, $func, $opts );
-               $this->assertEquals( $value, $v, "Value returned" );
-               $this->assertEquals( 1, $wasSet, "Value calculated" );
-               $v = $cache->getWithSetCallback( $key, 30, $func, $opts );
-               $this->assertEquals( 1, $wasSet, "Value cached" );
-
-               $asycList = [];
-               $asyncHandler = function ( $callback ) use ( &$asycList ) {
-                       $asycList[] = $callback;
-               };
-               $cache = new NearExpiringWANObjectCache( [
-                       'cache'        => new HashBagOStuff(),
-                       'asyncHandler' => $asyncHandler
-               ] );
-
-               $mockWallClock = 1549343530.2053;
-               $priorTime = $mockWallClock; // reference time
-               $cache->setMockTime( $mockWallClock );
-
-               $wasSet = 0;
-               $key = wfRandomString();
-               $opts = [ 'lowTTL' => 100 ];
-               $v = $cache->getWithSetCallback( $key, 300, $func, $opts );
-               $this->assertEquals( $value, $v, "Value returned" );
-               $this->assertEquals( 1, $wasSet, "Value calculated" );
-               $v = $cache->getWithSetCallback( $key, 300, $func, $opts );
-               $this->assertEquals( 1, $wasSet, "Cached value used" );
-               $this->assertEquals( $v, $value, "Value cached" );
-
-               $mockWallClock += 250;
-               $v = $cache->getWithSetCallback( $key, 300, $func, $opts );
-               $this->assertEquals( $value, $v, "Value returned" );
-               $this->assertEquals( 1, $wasSet, "Stale value used" );
-               $this->assertEquals( 1, count( $asycList ), "Refresh deferred." );
-               $value = 'NewCatsInTown'; // change callback return value
-               $asycList[0](); // run the refresh callback
-               $asycList = [];
-               $this->assertEquals( 2, $wasSet, "Value calculated at later time" );
-               $this->assertEquals( 0, count( $asycList ), "No deferred refreshes added." );
-               $v = $cache->getWithSetCallback( $key, 300, $func, $opts );
-               $this->assertEquals( $value, $v, "New value stored" );
-
-               $cache = new PopularityRefreshingWANObjectCache( [
-                       'cache'   => new HashBagOStuff()
-               ] );
-
-               $mockWallClock = $priorTime;
-               $cache->setMockTime( $mockWallClock );
-
-               $wasSet = 0;
-               $key = wfRandomString();
-               $opts = [ 'hotTTR' => 900 ];
-               $v = $cache->getWithSetCallback( $key, 60, $func, $opts );
-               $this->assertEquals( $value, $v, "Value returned" );
-               $this->assertEquals( 1, $wasSet, "Value calculated" );
-
-               $mockWallClock += 30;
-
-               $v = $cache->getWithSetCallback( $key, 60, $func, $opts );
-               $this->assertEquals( 1, $wasSet, "Value cached" );
-
-               $mockWallClock = $priorTime;
-               $wasSet = 0;
-               $key = wfRandomString();
-               $opts = [ 'hotTTR' => 10 ];
-               $v = $cache->getWithSetCallback( $key, 60, $func, $opts );
-               $this->assertEquals( $value, $v, "Value returned" );
-               $this->assertEquals( 1, $wasSet, "Value calculated" );
-
-               $mockWallClock += 30;
-
-               $v = $cache->getWithSetCallback( $key, 60, $func, $opts );
-               $this->assertEquals( $value, $v, "Value returned" );
-               $this->assertEquals( 2, $wasSet, "Value re-calculated" );
-       }
-
-       /**
-        * @covers WANObjectCache::getWithSetCallback()
-        * @covers WANObjectCache::doGetWithSetCallback()
-        */
-       public function testGetWithSetCallback_invalidCallback() {
-               $this->setExpectedException( InvalidArgumentException::class );
-               $this->cache->getWithSetCallback( 'key', 30, 'invalid callback' );
-       }
-
-       /**
-        * @dataProvider getMultiWithSetCallback_provider
-        * @covers WANObjectCache::getMultiWithSetCallback
-        * @covers WANObjectCache::makeMultiKeys
-        * @covers WANObjectCache::getMulti
-        * @param array $extOpts
-        * @param bool $versioned
-        */
-       public function testGetMultiWithSetCallback( array $extOpts, $versioned ) {
-               $cache = $this->cache;
-
-               $keyA = wfRandomString();
-               $keyB = wfRandomString();
-               $keyC = wfRandomString();
-               $cKey1 = wfRandomString();
-               $cKey2 = wfRandomString();
-
-               $priorValue = null;
-               $priorAsOf = null;
-               $wasSet = 0;
-               $genFunc = function ( $id, $old, &$ttl, &$opts, $asOf ) use (
-                       &$wasSet, &$priorValue, &$priorAsOf
-               ) {
-                       ++$wasSet;
-                       $priorValue = $old;
-                       $priorAsOf = $asOf;
-                       $ttl = 20; // override with another value
-                       return "@$id$";
-               };
-
-               $mockWallClock = 1549343530.2053;
-               $priorTime = $mockWallClock; // reference time
-               $cache->setMockTime( $mockWallClock );
-
-               $wasSet = 0;
-               $keyedIds = new ArrayIterator( [ $keyA => 3353 ] );
-               $value = "@3353$";
-               $v = $cache->getMultiWithSetCallback(
-                       $keyedIds, 30, $genFunc, [ 'lockTSE' => 5 ] + $extOpts );
-               $this->assertEquals( $value, $v[$keyA], "Value returned" );
-               $this->assertEquals( 1, $wasSet, "Value regenerated" );
-               $this->assertFalse( $priorValue, "No prior value" );
-               $this->assertNull( $priorAsOf, "No prior value" );
-
-               $curTTL = null;
-               $cache->get( $keyA, $curTTL );
-               $this->assertLessThanOrEqual( 20, $curTTL, 'Current TTL between 19-20 (overriden)' );
-               $this->assertGreaterThanOrEqual( 19, $curTTL, 'Current TTL between 19-20 (overriden)' );
-
-               $wasSet = 0;
-               $value = "@efef$";
-               $keyedIds = new ArrayIterator( [ $keyB => 'efef' ] );
-               $v = $cache->getMultiWithSetCallback(
-                       $keyedIds, 30, $genFunc, [ 'lowTTL' => 0, 'lockTSE' => 5, ] + $extOpts );
-               $this->assertEquals( $value, $v[$keyB], "Value returned" );
-               $this->assertEquals( 1, $wasSet, "Value regenerated" );
-               $this->assertEquals( 0, $cache->getWarmupKeyMisses(), "Keys warmed yet in process cache" );
-               $v = $cache->getMultiWithSetCallback(
-                       $keyedIds, 30, $genFunc, [ 'lowTTL' => 0, 'lockTSE' => 5, ] + $extOpts );
-               $this->assertEquals( $value, $v[$keyB], "Value returned" );
-               $this->assertEquals( 1, $wasSet, "Value not regenerated" );
-               $this->assertEquals( 0, $cache->getWarmupKeyMisses(), "Keys warmed in process cache" );
-
-               $mockWallClock += 1;
-
-               $wasSet = 0;
-               $keyedIds = new ArrayIterator( [ $keyB => 'efef' ] );
-               $v = $cache->getMultiWithSetCallback(
-                       $keyedIds, 30, $genFunc, [ 'checkKeys' => [ $cKey1, $cKey2 ] ] + $extOpts
-               );
-               $this->assertEquals( $value, $v[$keyB], "Value returned" );
-               $this->assertEquals( 1, $wasSet, "Value regenerated due to check keys" );
-               $this->assertEquals( $value, $priorValue, "Has prior value" );
-               $this->assertInternalType( 'float', $priorAsOf, "Has prior value" );
-               $t1 = $cache->getCheckKeyTime( $cKey1 );
-               $this->assertGreaterThanOrEqual( $priorTime, $t1, 'Check keys generated on miss' );
-               $t2 = $cache->getCheckKeyTime( $cKey2 );
-               $this->assertGreaterThanOrEqual( $priorTime, $t2, 'Check keys generated on miss' );
-
-               $mockWallClock += 0.01;
-               $priorTime = $mockWallClock;
-               $value = "@43636$";
-               $wasSet = 0;
-               $keyedIds = new ArrayIterator( [ $keyC => 43636 ] );
-               $v = $cache->getMultiWithSetCallback(
-                       $keyedIds, 30, $genFunc, [ 'checkKeys' => [ $cKey1, $cKey2 ] ] + $extOpts
-               );
-               $this->assertEquals( $value, $v[$keyC], "Value returned" );
-               $this->assertEquals( 1, $wasSet, "Value regenerated due to still-recent check keys" );
-               $t1 = $cache->getCheckKeyTime( $cKey1 );
-               $this->assertLessThanOrEqual( $priorTime, $t1, 'Check keys did not change again' );
-               $t2 = $cache->getCheckKeyTime( $cKey2 );
-               $this->assertLessThanOrEqual( $priorTime, $t2, 'Check keys did not change again' );
-
-               $curTTL = null;
-               $v = $cache->get( $keyC, $curTTL, [ $cKey1, $cKey2 ] );
-               if ( $versioned ) {
-                       $this->assertEquals( $value, $v[$cache::VFLD_DATA], "Value returned" );
-               } else {
-                       $this->assertEquals( $value, $v, "Value returned" );
-               }
-               $this->assertLessThanOrEqual( 0, $curTTL, "Value has current TTL < 0 due to check keys" );
-
-               $wasSet = 0;
-               $key = wfRandomString();
-               $keyedIds = new ArrayIterator( [ $key => 242424 ] );
-               $v = $cache->getMultiWithSetCallback(
-                       $keyedIds, 30, $genFunc, [ 'pcTTL' => 5 ] + $extOpts );
-               $this->assertEquals( "@{$keyedIds[$key]}$", $v[$key], "Value returned" );
-               $cache->delete( $key );
-               $keyedIds = new ArrayIterator( [ $key => 242424 ] );
-               $v = $cache->getMultiWithSetCallback(
-                       $keyedIds, 30, $genFunc, [ 'pcTTL' => 5 ] + $extOpts );
-               $this->assertEquals( "@{$keyedIds[$key]}$", $v[$key], "Value still returned after deleted" );
-               $this->assertEquals( 1, $wasSet, "Value process cached while deleted" );
-
-               $calls = 0;
-               $ids = [ 1, 2, 3, 4, 5, 6 ];
-               $keyFunc = function ( $id, WANObjectCache $wanCache ) {
-                       return $wanCache->makeKey( 'test', $id );
-               };
-               $keyedIds = $cache->makeMultiKeys( $ids, $keyFunc );
-               $genFunc = function ( $id, $oldValue, &$ttl, array &$setops ) use ( &$calls ) {
-                       ++$calls;
-
-                       return "val-{$id}";
-               };
-               $values = $cache->getMultiWithSetCallback( $keyedIds, 10, $genFunc );
-
-               $this->assertEquals(
-                       [ "val-1", "val-2", "val-3", "val-4", "val-5", "val-6" ],
-                       array_values( $values ),
-                       "Correct values in correct order"
-               );
-               $this->assertEquals(
-                       array_map( $keyFunc, $ids, array_fill( 0, count( $ids ), $this->cache ) ),
-                       array_keys( $values ),
-                       "Correct keys in correct order"
-               );
-               $this->assertEquals( count( $ids ), $calls );
-
-               $cache->getMultiWithSetCallback( $keyedIds, 10, $genFunc );
-               $this->assertEquals( count( $ids ), $calls, "Values cached" );
-
-               // Mock the BagOStuff to assure only one getMulti() call given process caching
-               $localBag = $this->getMockBuilder( HashBagOStuff::class )
-                       ->setMethods( [ 'getMulti' ] )->getMock();
-               $localBag->expects( $this->exactly( 1 ) )->method( 'getMulti' )->willReturn( [
-                       WANObjectCache::VALUE_KEY_PREFIX . 'k1' => 'val-id1',
-                       WANObjectCache::VALUE_KEY_PREFIX . 'k2' => 'val-id2'
-               ] );
-               $wanCache = new WANObjectCache( [ 'cache' => $localBag ] );
-
-               // Warm the process cache
-               $keyedIds = new ArrayIterator( [ 'k1' => 'id1', 'k2' => 'id2' ] );
-               $this->assertEquals(
-                       [ 'k1' => 'val-id1', 'k2' => 'val-id2' ],
-                       $wanCache->getMultiWithSetCallback( $keyedIds, 10, $genFunc, [ 'pcTTL' => 5 ] )
-               );
-               // Use the process cache
-               $this->assertEquals(
-                       [ 'k1' => 'val-id1', 'k2' => 'val-id2' ],
-                       $wanCache->getMultiWithSetCallback( $keyedIds, 10, $genFunc, [ 'pcTTL' => 5 ] )
-               );
-       }
-
-       public static function getMultiWithSetCallback_provider() {
-               return [
-                       [ [], false ],
-                       [ [ 'version' => 1 ], true ]
-               ];
-       }
-
-       /**
-        * @dataProvider getMultiWithUnionSetCallback_provider
-        * @covers WANObjectCache::getMultiWithUnionSetCallback()
-        * @covers WANObjectCache::makeMultiKeys()
-        * @param array $extOpts
-        * @param bool $versioned
-        */
-       public function testGetMultiWithUnionSetCallback( array $extOpts, $versioned ) {
-               $cache = $this->cache;
-
-               $keyA = wfRandomString();
-               $keyB = wfRandomString();
-               $keyC = wfRandomString();
-               $cKey1 = wfRandomString();
-               $cKey2 = wfRandomString();
-
-               $wasSet = 0;
-               $genFunc = function ( array $ids, array &$ttls, array &$setOpts ) use (
-                       &$wasSet, &$priorValue, &$priorAsOf
-               ) {
-                       $newValues = [];
-                       foreach ( $ids as $id ) {
-                               ++$wasSet;
-                               $newValues[$id] = "@$id$";
-                               $ttls[$id] = 20; // override with another value
-                       }
-
-                       return $newValues;
-               };
-
-               $mockWallClock = 1549343530.2053;
-               $priorTime = $mockWallClock; // reference time
-               $cache->setMockTime( $mockWallClock );
-
-               $wasSet = 0;
-               $keyedIds = new ArrayIterator( [ $keyA => 3353 ] );
-               $value = "@3353$";
-               $v = $cache->getMultiWithUnionSetCallback(
-                       $keyedIds, 30, $genFunc, $extOpts );
-               $this->assertEquals( $value, $v[$keyA], "Value returned" );
-               $this->assertEquals( 1, $wasSet, "Value regenerated" );
-
-               $curTTL = null;
-               $cache->get( $keyA, $curTTL );
-               $this->assertLessThanOrEqual( 20, $curTTL, 'Current TTL between 19-20 (overriden)' );
-               $this->assertGreaterThanOrEqual( 19, $curTTL, 'Current TTL between 19-20 (overriden)' );
-
-               $wasSet = 0;
-               $value = "@efef$";
-               $keyedIds = new ArrayIterator( [ $keyB => 'efef' ] );
-               $v = $cache->getMultiWithUnionSetCallback(
-                       $keyedIds, 30, $genFunc, [ 'lowTTL' => 0 ] + $extOpts );
-               $this->assertEquals( $value, $v[$keyB], "Value returned" );
-               $this->assertEquals( 1, $wasSet, "Value regenerated" );
-               $this->assertEquals( 0, $cache->getWarmupKeyMisses(), "Keys warmed yet in process cache" );
-               $v = $cache->getMultiWithUnionSetCallback(
-                       $keyedIds, 30, $genFunc, [ 'lowTTL' => 0 ] + $extOpts );
-               $this->assertEquals( $value, $v[$keyB], "Value returned" );
-               $this->assertEquals( 1, $wasSet, "Value not regenerated" );
-               $this->assertEquals( 0, $cache->getWarmupKeyMisses(), "Keys warmed in process cache" );
-
-               $mockWallClock += 1;
-
-               $wasSet = 0;
-               $keyedIds = new ArrayIterator( [ $keyB => 'efef' ] );
-               $v = $cache->getMultiWithUnionSetCallback(
-                       $keyedIds, 30, $genFunc, [ 'checkKeys' => [ $cKey1, $cKey2 ] ] + $extOpts
-               );
-               $this->assertEquals( $value, $v[$keyB], "Value returned" );
-               $this->assertEquals( 1, $wasSet, "Value regenerated due to check keys" );
-               $t1 = $cache->getCheckKeyTime( $cKey1 );
-               $this->assertGreaterThanOrEqual( $priorTime, $t1, 'Check keys generated on miss' );
-               $t2 = $cache->getCheckKeyTime( $cKey2 );
-               $this->assertGreaterThanOrEqual( $priorTime, $t2, 'Check keys generated on miss' );
-
-               $mockWallClock += 0.01;
-               $priorTime = $mockWallClock;
-               $value = "@43636$";
-               $wasSet = 0;
-               $keyedIds = new ArrayIterator( [ $keyC => 43636 ] );
-               $v = $cache->getMultiWithUnionSetCallback(
-                       $keyedIds, 30, $genFunc, [ 'checkKeys' => [ $cKey1, $cKey2 ] ] + $extOpts
-               );
-               $this->assertEquals( $value, $v[$keyC], "Value returned" );
-               $this->assertEquals( 1, $wasSet, "Value regenerated due to still-recent check keys" );
-               $t1 = $cache->getCheckKeyTime( $cKey1 );
-               $this->assertLessThanOrEqual( $priorTime, $t1, 'Check keys did not change again' );
-               $t2 = $cache->getCheckKeyTime( $cKey2 );
-               $this->assertLessThanOrEqual( $priorTime, $t2, 'Check keys did not change again' );
-
-               $curTTL = null;
-               $v = $cache->get( $keyC, $curTTL, [ $cKey1, $cKey2 ] );
-               if ( $versioned ) {
-                       $this->assertEquals( $value, $v[$cache::VFLD_DATA], "Value returned" );
-               } else {
-                       $this->assertEquals( $value, $v, "Value returned" );
-               }
-               $this->assertLessThanOrEqual( 0, $curTTL, "Value has current TTL < 0 due to check keys" );
-
-               $wasSet = 0;
-               $key = wfRandomString();
-               $keyedIds = new ArrayIterator( [ $key => 242424 ] );
-               $v = $cache->getMultiWithUnionSetCallback(
-                       $keyedIds, 30, $genFunc, [ 'pcTTL' => 5 ] + $extOpts );
-               $this->assertEquals( "@{$keyedIds[$key]}$", $v[$key], "Value returned" );
-               $cache->delete( $key );
-               $keyedIds = new ArrayIterator( [ $key => 242424 ] );
-               $v = $cache->getMultiWithUnionSetCallback(
-                       $keyedIds, 30, $genFunc, [ 'pcTTL' => 5 ] + $extOpts );
-               $this->assertEquals( "@{$keyedIds[$key]}$", $v[$key], "Value still returned after deleted" );
-               $this->assertEquals( 1, $wasSet, "Value process cached while deleted" );
-
-               $calls = 0;
-               $ids = [ 1, 2, 3, 4, 5, 6 ];
-               $keyFunc = function ( $id, WANObjectCache $wanCache ) {
-                       return $wanCache->makeKey( 'test', $id );
-               };
-               $keyedIds = $cache->makeMultiKeys( $ids, $keyFunc );
-               $genFunc = function ( array $ids, array &$ttls, array &$setOpts ) use ( &$calls ) {
-                       $newValues = [];
-                       foreach ( $ids as $id ) {
-                               ++$calls;
-                               $newValues[$id] = "val-{$id}";
-                       }
-
-                       return $newValues;
-               };
-               $values = $cache->getMultiWithUnionSetCallback( $keyedIds, 10, $genFunc );
-
-               $this->assertEquals(
-                       [ "val-1", "val-2", "val-3", "val-4", "val-5", "val-6" ],
-                       array_values( $values ),
-                       "Correct values in correct order"
-               );
-               $this->assertEquals(
-                       array_map( $keyFunc, $ids, array_fill( 0, count( $ids ), $this->cache ) ),
-                       array_keys( $values ),
-                       "Correct keys in correct order"
-               );
-               $this->assertEquals( count( $ids ), $calls );
-
-               $cache->getMultiWithUnionSetCallback( $keyedIds, 10, $genFunc );
-               $this->assertEquals( count( $ids ), $calls, "Values cached" );
-       }
-
-       public static function getMultiWithUnionSetCallback_provider() {
-               return [
-                       [ [], false ],
-                       [ [ 'version' => 1 ], true ]
-               ];
-       }
-
-       /**
-        * @covers WANObjectCache::getWithSetCallback()
-        * @covers WANObjectCache::doGetWithSetCallback()
-        */
-       public function testLockTSE() {
-               $cache = $this->cache;
-               $key = wfRandomString();
-               $value = wfRandomString();
-
-               $mockWallClock = 1549343530.2053;
-               $cache->setMockTime( $mockWallClock );
-
-               $calls = 0;
-               $func = function () use ( &$calls, $value, $cache, $key ) {
-                       ++$calls;
-                       return $value;
-               };
-
-               $ret = $cache->getWithSetCallback( $key, 30, $func, [ 'lockTSE' => 5 ] );
-               $this->assertEquals( $value, $ret );
-               $this->assertEquals( 1, $calls, 'Value was populated' );
-
-               // Acquire the mutex to verify that getWithSetCallback uses lockTSE properly
-               $this->internalCache->add( $cache::MUTEX_KEY_PREFIX . $key, 1, 0 );
-
-               $checkKeys = [ wfRandomString() ]; // new check keys => force misses
-               $ret = $cache->getWithSetCallback( $key, 30, $func,
-                       [ 'lockTSE' => 5, 'checkKeys' => $checkKeys ] );
-               $this->assertEquals( $value, $ret, 'Old value used' );
-               $this->assertEquals( 1, $calls, 'Callback was not used' );
-
-               $cache->delete( $key );
-               $mockWallClock += 0.001; // cached values will be newer than tombstone
-               $ret = $cache->getWithSetCallback( $key, 30, $func,
-                       [ 'lockTSE' => 5, 'checkKeys' => $checkKeys ] );
-               $this->assertEquals( $value, $ret, 'Callback was used; interim saved' );
-               $this->assertEquals( 2, $calls, 'Callback was used; interim saved' );
-
-               $ret = $cache->getWithSetCallback( $key, 30, $func,
-                       [ 'lockTSE' => 5, 'checkKeys' => $checkKeys ] );
-               $this->assertEquals( $value, $ret, 'Callback was not used; used interim (mutex failed)' );
-               $this->assertEquals( 2, $calls, 'Callback was not used; used interim (mutex failed)' );
-       }
-
-       /**
-        * @covers WANObjectCache::getWithSetCallback()
-        * @covers WANObjectCache::doGetWithSetCallback()
-        * @covers WANObjectCache::set()
-        */
-       public function testLockTSESlow() {
-               $cache = $this->cache;
-               $key = wfRandomString();
-               $key2 = wfRandomString();
-               $value = wfRandomString();
-
-               $mockWallClock = 1549343530.2053;
-               $cache->setMockTime( $mockWallClock );
-
-               $calls = 0;
-               $func = function ( $oldValue, &$ttl, &$setOpts ) use ( &$calls, $value, &$mockWallClock ) {
-                       ++$calls;
-                       $setOpts['since'] = $mockWallClock - 10;
-                       return $value;
-               };
-
-               // Value should be given a low logical TTL due to snapshot lag
-               $curTTL = null;
-               $ret = $cache->getWithSetCallback( $key, 300, $func, [ 'lockTSE' => 5 ] );
-               $this->assertEquals( $value, $ret );
-               $this->assertEquals( $value, $cache->get( $key, $curTTL ), 'Value was populated' );
-               $this->assertEquals( 1, $curTTL, 'Value has reduced logical TTL', 0.01 );
-               $this->assertEquals( 1, $calls, 'Value was generated' );
-
-               $mockWallClock += 2; // low logical TTL expired
-
-               $ret = $cache->getWithSetCallback( $key, 300, $func, [ 'lockTSE' => 5 ] );
-               $this->assertEquals( $value, $ret );
-               $this->assertEquals( 2, $calls, 'Callback used (mutex acquired)' );
-
-               $ret = $cache->getWithSetCallback( $key, 300, $func, [ 'lockTSE' => 5 ] );
-               $this->assertEquals( $value, $ret );
-               $this->assertEquals( 2, $calls, 'Callback was not used (interim value used)' );
-
-               $mockWallClock += 2; // low logical TTL expired
-               // Acquire a lock to verify that getWithSetCallback uses lockTSE properly
-               $this->internalCache->add( $cache::MUTEX_KEY_PREFIX . $key, 1, 0 );
-
-               $ret = $cache->getWithSetCallback( $key, 300, $func, [ 'lockTSE' => 5 ] );
-               $this->assertEquals( $value, $ret );
-               $this->assertEquals( 2, $calls, 'Callback was not used (mutex not acquired)' );
-
-               $mockWallClock += 301; // physical TTL expired
-               // Acquire a lock to verify that getWithSetCallback uses lockTSE properly
-               $this->internalCache->add( $cache::MUTEX_KEY_PREFIX . $key, 1, 0 );
-
-               $ret = $cache->getWithSetCallback( $key, 300, $func, [ 'lockTSE' => 5 ] );
-               $this->assertEquals( $value, $ret );
-               $this->assertEquals( 3, $calls, 'Callback was used (mutex not acquired, not in cache)' );
-
-               $calls = 0;
-               $func2 = function ( $oldValue, &$ttl, &$setOpts ) use ( &$calls, $value ) {
-                       ++$calls;
-                       $setOpts['lag'] = 15;
-                       return $value;
-               };
-
-               // Value should be given a low logical TTL due to replication lag
-               $curTTL = null;
-               $ret = $cache->getWithSetCallback( $key2, 300, $func2, [ 'lockTSE' => 5 ] );
-               $this->assertEquals( $value, $ret );
-               $this->assertEquals( $value, $cache->get( $key2, $curTTL ), 'Value was populated' );
-               $this->assertEquals( 30, $curTTL, 'Value has reduced logical TTL', 0.01 );
-               $this->assertEquals( 1, $calls, 'Value was generated' );
-
-               $ret = $cache->getWithSetCallback( $key2, 300, $func2, [ 'lockTSE' => 5 ] );
-               $this->assertEquals( $value, $ret );
-               $this->assertEquals( 1, $calls, 'Callback was used (not expired)' );
-
-               $mockWallClock += 31;
-
-               $ret = $cache->getWithSetCallback( $key2, 300, $func2, [ 'lockTSE' => 5 ] );
-               $this->assertEquals( $value, $ret );
-               $this->assertEquals( 2, $calls, 'Callback was used (mutex acquired)' );
-       }
-
-       /**
-        * @covers WANObjectCache::getWithSetCallback()
-        * @covers WANObjectCache::doGetWithSetCallback()
-        */
-       public function testBusyValue() {
-               $cache = $this->cache;
-               $key = wfRandomString();
-               $value = wfRandomString();
-               $busyValue = wfRandomString();
-
-               $mockWallClock = 1549343530.2053;
-               $cache->setMockTime( $mockWallClock );
-
-               $calls = 0;
-               $func = function () use ( &$calls, $value, $cache, $key ) {
-                       ++$calls;
-                       return $value;
-               };
-
-               $ret = $cache->getWithSetCallback( $key, 30, $func, [ 'busyValue' => $busyValue ] );
-               $this->assertEquals( $value, $ret );
-               $this->assertEquals( 1, $calls, 'Value was populated' );
-
-               $mockWallClock += 0.2; // interim keys not brand new
-
-               // Acquire a lock to verify that getWithSetCallback uses busyValue properly
-               $this->internalCache->add( $cache::MUTEX_KEY_PREFIX . $key, 1, 0 );
-
-               $checkKeys = [ wfRandomString() ]; // new check keys => force misses
-               $ret = $cache->getWithSetCallback( $key, 30, $func,
-                       [ 'busyValue' => $busyValue, 'checkKeys' => $checkKeys ] );
-               $this->assertEquals( $value, $ret, 'Callback used' );
-               $this->assertEquals( 2, $calls, 'Callback used' );
-
-               $ret = $cache->getWithSetCallback( $key, 30, $func,
-                       [ 'lockTSE' => 30, 'busyValue' => $busyValue, 'checkKeys' => $checkKeys ] );
-               $this->assertEquals( $value, $ret, 'Old value used' );
-               $this->assertEquals( 2, $calls, 'Callback was not used' );
-
-               $cache->delete( $key ); // no value at all anymore and still locked
-               $ret = $cache->getWithSetCallback( $key, 30, $func,
-                       [ 'busyValue' => $busyValue, 'checkKeys' => $checkKeys ] );
-               $this->assertEquals( $busyValue, $ret, 'Callback was not used; used busy value' );
-               $this->assertEquals( 2, $calls, 'Callback was not used; used busy value' );
-
-               $this->internalCache->delete( $cache::MUTEX_KEY_PREFIX . $key );
-               $mockWallClock += 0.001; // cached values will be newer than tombstone
-               $ret = $cache->getWithSetCallback( $key, 30, $func,
-                       [ 'lockTSE' => 30, 'busyValue' => $busyValue, 'checkKeys' => $checkKeys ] );
-               $this->assertEquals( $value, $ret, 'Callback was used; saved interim' );
-               $this->assertEquals( 3, $calls, 'Callback was used; saved interim' );
-
-               $this->internalCache->add( $cache::MUTEX_KEY_PREFIX . $key, 1, 0 );
-               $ret = $cache->getWithSetCallback( $key, 30, $func,
-                       [ 'busyValue' => $busyValue, 'checkKeys' => $checkKeys ] );
-               $this->assertEquals( $value, $ret, 'Callback was not used; used interim' );
-               $this->assertEquals( 3, $calls, 'Callback was not used; used interim' );
-       }
-
-       /**
-        * @covers WANObjectCache::getMulti()
-        */
-       public function testGetMulti() {
-               $cache = $this->cache;
-
-               $value1 = [ 'this' => 'is', 'a' => 'test' ];
-               $value2 = [ 'this' => 'is', 'another' => 'test' ];
-
-               $key1 = wfRandomString();
-               $key2 = wfRandomString();
-               $key3 = wfRandomString();
-
-               $mockWallClock = 1549343530.2053;
-               $priorTime = $mockWallClock; // reference time
-               $cache->setMockTime( $mockWallClock );
-
-               $cache->set( $key1, $value1, 5 );
-               $cache->set( $key2, $value2, 10 );
-
-               $curTTLs = [];
-               $this->assertEquals(
-                       [ $key1 => $value1, $key2 => $value2 ],
-                       $cache->getMulti( [ $key1, $key2, $key3 ], $curTTLs ),
-                       'Result array populated'
-               );
-
-               $this->assertEquals( 2, count( $curTTLs ), "Two current TTLs in array" );
-               $this->assertGreaterThan( 0, $curTTLs[$key1], "Key 1 has current TTL > 0" );
-               $this->assertGreaterThan( 0, $curTTLs[$key2], "Key 2 has current TTL > 0" );
-
-               $cKey1 = wfRandomString();
-               $cKey2 = wfRandomString();
-
-               $mockWallClock += 1;
-
-               $curTTLs = [];
-               $this->assertEquals(
-                       [ $key1 => $value1, $key2 => $value2 ],
-                       $cache->getMulti( [ $key1, $key2, $key3 ], $curTTLs, [ $cKey1, $cKey2 ] ),
-                       "Result array populated even with new check keys"
-               );
-               $t1 = $cache->getCheckKeyTime( $cKey1 );
-               $this->assertGreaterThanOrEqual( $priorTime, $t1, 'Check key 1 generated on miss' );
-               $t2 = $cache->getCheckKeyTime( $cKey2 );
-               $this->assertGreaterThanOrEqual( $priorTime, $t2, 'Check key 2 generated on miss' );
-               $this->assertEquals( 2, count( $curTTLs ), "Current TTLs array set" );
-               $this->assertLessThanOrEqual( 0, $curTTLs[$key1], 'Key 1 has current TTL <= 0' );
-               $this->assertLessThanOrEqual( 0, $curTTLs[$key2], 'Key 2 has current TTL <= 0' );
-
-               $mockWallClock += 1;
-
-               $curTTLs = [];
-               $this->assertEquals(
-                       [ $key1 => $value1, $key2 => $value2 ],
-                       $cache->getMulti( [ $key1, $key2, $key3 ], $curTTLs, [ $cKey1, $cKey2 ] ),
-                       "Result array still populated even with new check keys"
-               );
-               $this->assertEquals( 2, count( $curTTLs ), "Current TTLs still array set" );
-               $this->assertLessThan( 0, $curTTLs[$key1], 'Key 1 has negative current TTL' );
-               $this->assertLessThan( 0, $curTTLs[$key2], 'Key 2 has negative current TTL' );
-       }
-
-       /**
-        * @covers WANObjectCache::getMulti()
-        * @covers WANObjectCache::processCheckKeys()
-        */
-       public function testGetMultiCheckKeys() {
-               $cache = $this->cache;
-
-               $checkAll = wfRandomString();
-               $check1 = wfRandomString();
-               $check2 = wfRandomString();
-               $check3 = wfRandomString();
-               $value1 = wfRandomString();
-               $value2 = wfRandomString();
-
-               $mockWallClock = 1549343530.2053;
-               $cache->setMockTime( $mockWallClock );
-
-               // Fake initial check key to be set in the past. Otherwise we'd have to sleep for
-               // several seconds during the test to assert the behaviour.
-               foreach ( [ $checkAll, $check1, $check2 ] as $checkKey ) {
-                       $cache->touchCheckKey( $checkKey, WANObjectCache::HOLDOFF_NONE );
-               }
-
-               $mockWallClock += 0.100;
-
-               $cache->set( 'key1', $value1, 10 );
-               $cache->set( 'key2', $value2, 10 );
-
-               $curTTLs = [];
-               $result = $cache->getMulti( [ 'key1', 'key2', 'key3' ], $curTTLs, [
-                       'key1' => $check1,
-                       $checkAll,
-                       'key2' => $check2,
-                       'key3' => $check3,
-               ] );
-               $this->assertEquals(
-                       [ 'key1' => $value1, 'key2' => $value2 ],
-                       $result,
-                       'Initial values'
-               );
-               $this->assertGreaterThanOrEqual( 9.5, $curTTLs['key1'], 'Initial ttls' );
-               $this->assertLessThanOrEqual( 10.5, $curTTLs['key1'], 'Initial ttls' );
-               $this->assertGreaterThanOrEqual( 9.5, $curTTLs['key2'], 'Initial ttls' );
-               $this->assertLessThanOrEqual( 10.5, $curTTLs['key2'], 'Initial ttls' );
-
-               $mockWallClock += 0.100;
-               $cache->touchCheckKey( $check1 );
-
-               $curTTLs = [];
-               $result = $cache->getMulti( [ 'key1', 'key2', 'key3' ], $curTTLs, [
-                       'key1' => $check1,
-                       $checkAll,
-                       'key2' => $check2,
-                       'key3' => $check3,
-               ] );
-               $this->assertEquals(
-                       [ 'key1' => $value1, 'key2' => $value2 ],
-                       $result,
-                       'key1 expired by check1, but value still provided'
-               );
-               $this->assertLessThan( 0, $curTTLs['key1'], 'key1 TTL expired' );
-               $this->assertGreaterThan( 0, $curTTLs['key2'], 'key2 still valid' );
-
-               $cache->touchCheckKey( $checkAll );
-
-               $curTTLs = [];
-               $result = $cache->getMulti( [ 'key1', 'key2', 'key3' ], $curTTLs, [
-                       'key1' => $check1,
-                       $checkAll,
-                       'key2' => $check2,
-                       'key3' => $check3,
-               ] );
-               $this->assertEquals(
-                       [ 'key1' => $value1, 'key2' => $value2 ],
-                       $result,
-                       'All keys expired by checkAll, but value still provided'
-               );
-               $this->assertLessThan( 0, $curTTLs['key1'], 'key1 expired by checkAll' );
-               $this->assertLessThan( 0, $curTTLs['key2'], 'key2 expired by checkAll' );
-       }
-
-       /**
-        * @covers WANObjectCache::get()
-        * @covers WANObjectCache::processCheckKeys()
-        */
-       public function testCheckKeyInitHoldoff() {
-               $cache = $this->cache;
-
-               for ( $i = 0; $i < 500; ++$i ) {
-                       $key = wfRandomString();
-                       $checkKey = wfRandomString();
-                       // miss, set, hit
-                       $cache->get( $key, $curTTL, [ $checkKey ] );
-                       $cache->set( $key, 'val', 10 );
-                       $curTTL = null;
-                       $v = $cache->get( $key, $curTTL, [ $checkKey ] );
-
-                       $this->assertEquals( 'val', $v );
-                       $this->assertLessThan( 0, $curTTL, "Step $i: CTL < 0 (miss/set/hit)" );
-               }
-
-               for ( $i = 0; $i < 500; ++$i ) {
-                       $key = wfRandomString();
-                       $checkKey = wfRandomString();
-                       // set, hit
-                       $cache->set( $key, 'val', 10 );
-                       $curTTL = null;
-                       $v = $cache->get( $key, $curTTL, [ $checkKey ] );
-
-                       $this->assertEquals( 'val', $v );
-                       $this->assertLessThan( 0, $curTTL, "Step $i: CTL < 0 (set/hit)" );
-               }
-       }
-
-       /**
-        * @covers WANObjectCache::delete
-        * @covers WANObjectCache::relayDelete
-        * @covers WANObjectCache::relayPurge
-        */
-       public function testDelete() {
-               $key = wfRandomString();
-               $value = wfRandomString();
-               $this->cache->set( $key, $value );
-
-               $curTTL = null;
-               $v = $this->cache->get( $key, $curTTL );
-               $this->assertEquals( $value, $v, "Key was created with value" );
-               $this->assertGreaterThan( 0, $curTTL, "Existing key has current TTL > 0" );
-
-               $this->cache->delete( $key );
-
-               $curTTL = null;
-               $v = $this->cache->get( $key, $curTTL );
-               $this->assertFalse( $v, "Deleted key has false value" );
-               $this->assertLessThan( 0, $curTTL, "Deleted key has current TTL < 0" );
-
-               $this->cache->set( $key, $value . 'more' );
-               $v = $this->cache->get( $key, $curTTL );
-               $this->assertFalse( $v, "Deleted key is tombstoned and has false value" );
-               $this->assertLessThan( 0, $curTTL, "Deleted key is tombstoned and has current TTL < 0" );
-
-               $this->cache->set( $key, $value );
-               $this->cache->delete( $key, WANObjectCache::HOLDOFF_NONE );
-
-               $curTTL = null;
-               $v = $this->cache->get( $key, $curTTL );
-               $this->assertFalse( $v, "Deleted key has false value" );
-               $this->assertNull( $curTTL, "Deleted key has null current TTL" );
-
-               $this->cache->set( $key, $value );
-               $v = $this->cache->get( $key, $curTTL );
-               $this->assertEquals( $value, $v, "Key was created with value" );
-               $this->assertGreaterThan( 0, $curTTL, "Existing key has current TTL > 0" );
-       }
-
-       /**
-        * @dataProvider getWithSetCallback_versions_provider
-        * @covers WANObjectCache::getWithSetCallback()
-        * @covers WANObjectCache::doGetWithSetCallback()
-        * @param array $extOpts
-        * @param bool $versioned
-        */
-       public function testGetWithSetCallback_versions( array $extOpts, $versioned ) {
-               $cache = $this->cache;
-
-               $key = wfRandomString();
-               $valueV1 = wfRandomString();
-               $valueV2 = [ wfRandomString() ];
-
-               $wasSet = 0;
-               $funcV1 = function () use ( &$wasSet, $valueV1 ) {
-                       ++$wasSet;
-
-                       return $valueV1;
-               };
-
-               $priorValue = false;
-               $priorAsOf = null;
-               $funcV2 = function ( $oldValue, &$ttl, $setOpts, $oldAsOf )
-               use ( &$wasSet, $valueV2, &$priorValue, &$priorAsOf ) {
-                       $priorValue = $oldValue;
-                       $priorAsOf = $oldAsOf;
-                       ++$wasSet;
-
-                       return $valueV2; // new array format
-               };
-
-               // Set the main key (version N if versioned)
-               $wasSet = 0;
-               $v = $cache->getWithSetCallback( $key, 30, $funcV1, $extOpts );
-               $this->assertEquals( $valueV1, $v, "Value returned" );
-               $this->assertEquals( 1, $wasSet, "Value regenerated" );
-               $cache->getWithSetCallback( $key, 30, $funcV1, $extOpts );
-               $this->assertEquals( 1, $wasSet, "Value not regenerated" );
-               $this->assertEquals( $valueV1, $v, "Value not regenerated" );
-
-               if ( $versioned ) {
-                       // Set the key for version N+1 format
-                       $verOpts = [ 'version' => $extOpts['version'] + 1 ];
-               } else {
-                       // Start versioning now with the unversioned key still there
-                       $verOpts = [ 'version' => 1 ];
-               }
-
-               // Value goes to secondary key since V1 already used $key
-               $wasSet = 0;
-               $v = $cache->getWithSetCallback( $key, 30, $funcV2, $verOpts + $extOpts );
-               $this->assertEquals( $valueV2, $v, "Value returned" );
-               $this->assertEquals( 1, $wasSet, "Value regenerated" );
-               $this->assertEquals( false, $priorValue, "Old value not given due to old format" );
-               $this->assertEquals( null, $priorAsOf, "Old value not given due to old format" );
-
-               $wasSet = 0;
-               $v = $cache->getWithSetCallback( $key, 30, $funcV2, $verOpts + $extOpts );
-               $this->assertEquals( $valueV2, $v, "Value not regenerated (secondary key)" );
-               $this->assertEquals( 0, $wasSet, "Value not regenerated (secondary key)" );
-
-               // Clear out the older or unversioned key
-               $cache->delete( $key, 0 );
-
-               // Set the key for next/first versioned format
-               $wasSet = 0;
-               $v = $cache->getWithSetCallback( $key, 30, $funcV2, $verOpts + $extOpts );
-               $this->assertEquals( $valueV2, $v, "Value returned" );
-               $this->assertEquals( 1, $wasSet, "Value regenerated" );
-
-               $v = $cache->getWithSetCallback( $key, 30, $funcV2, $verOpts + $extOpts );
-               $this->assertEquals( $valueV2, $v, "Value not regenerated (main key)" );
-               $this->assertEquals( 1, $wasSet, "Value not regenerated (main key)" );
-       }
-
-       public static function getWithSetCallback_versions_provider() {
-               return [
-                       [ [], false ],
-                       [ [ 'version' => 1 ], true ]
-               ];
-       }
-
-       /**
-        * @covers WANObjectCache::useInterimHoldOffCaching
-        * @covers WANObjectCache::getInterimValue
-        */
-       public function testInterimHoldOffCaching() {
-               $cache = $this->cache;
-
-               $mockWallClock = 1549343530.2053;
-               $cache->setMockTime( $mockWallClock );
-
-               $value = 'CRL-40-940';
-               $wasCalled = 0;
-               $func = function () use ( &$wasCalled, $value ) {
-                       $wasCalled++;
-
-                       return $value;
-               };
-
-               $cache->useInterimHoldOffCaching( true );
-
-               $key = wfRandomString( 32 );
-               $v = $cache->getWithSetCallback( $key, 60, $func );
-               $v = $cache->getWithSetCallback( $key, 60, $func );
-               $this->assertEquals( 1, $wasCalled, 'Value cached' );
-
-               $cache->delete( $key );
-               $mockWallClock += 0.001; // cached values will be newer than tombstone
-               $v = $cache->getWithSetCallback( $key, 60, $func );
-               $this->assertEquals( 2, $wasCalled, 'Value regenerated (got mutex)' ); // sets interim
-               $v = $cache->getWithSetCallback( $key, 60, $func );
-               $this->assertEquals( 2, $wasCalled, 'Value interim cached' ); // reuses interim
-
-               $mockWallClock += 0.2; // interim key not brand new
-               $v = $cache->getWithSetCallback( $key, 60, $func );
-               $this->assertEquals( 3, $wasCalled, 'Value regenerated (got mutex)' ); // sets interim
-               // Lock up the mutex so interim cache is used
-               $this->internalCache->add( $cache::MUTEX_KEY_PREFIX . $key, 1, 0 );
-               $v = $cache->getWithSetCallback( $key, 60, $func );
-               $this->assertEquals( 3, $wasCalled, 'Value interim cached (failed mutex)' );
-               $this->internalCache->delete( $cache::MUTEX_KEY_PREFIX . $key );
-
-               $cache->useInterimHoldOffCaching( false );
-
-               $wasCalled = 0;
-               $key = wfRandomString( 32 );
-               $v = $cache->getWithSetCallback( $key, 60, $func );
-               $v = $cache->getWithSetCallback( $key, 60, $func );
-               $this->assertEquals( 1, $wasCalled, 'Value cached' );
-               $cache->delete( $key );
-               $v = $cache->getWithSetCallback( $key, 60, $func );
-               $this->assertEquals( 2, $wasCalled, 'Value regenerated (got mutex)' );
-               $v = $cache->getWithSetCallback( $key, 60, $func );
-               $this->assertEquals( 3, $wasCalled, 'Value still regenerated (got mutex)' );
-               $v = $cache->getWithSetCallback( $key, 60, $func );
-               $this->assertEquals( 4, $wasCalled, 'Value still regenerated (got mutex)' );
-               // Lock up the mutex so interim cache is used
-               $this->internalCache->add( $cache::MUTEX_KEY_PREFIX . $key, 1, 0 );
-               $v = $cache->getWithSetCallback( $key, 60, $func );
-               $this->assertEquals( 5, $wasCalled, 'Value still regenerated (failed mutex)' );
-       }
-
-       /**
-        * @covers WANObjectCache::touchCheckKey
-        * @covers WANObjectCache::resetCheckKey
-        * @covers WANObjectCache::getCheckKeyTime
-        * @covers WANObjectCache::getMultiCheckKeyTime
-        * @covers WANObjectCache::makePurgeValue
-        * @covers WANObjectCache::parsePurgeValue
-        */
-       public function testTouchKeys() {
-               $cache = $this->cache;
-               $key = wfRandomString();
-
-               $mockWallClock = 1549343530.2053;
-               $priorTime = $mockWallClock; // reference time
-               $cache->setMockTime( $mockWallClock );
-
-               $mockWallClock += 0.100;
-               $t0 = $cache->getCheckKeyTime( $key );
-               $this->assertGreaterThanOrEqual( $priorTime, $t0, 'Check key auto-created' );
-
-               $priorTime = $mockWallClock;
-               $mockWallClock += 0.100;
-               $cache->touchCheckKey( $key );
-               $t1 = $cache->getCheckKeyTime( $key );
-               $this->assertGreaterThanOrEqual( $priorTime, $t1, 'Check key created' );
-
-               $t2 = $cache->getCheckKeyTime( $key );
-               $this->assertEquals( $t1, $t2, 'Check key time did not change' );
-
-               $mockWallClock += 0.100;
-               $cache->touchCheckKey( $key );
-               $t3 = $cache->getCheckKeyTime( $key );
-               $this->assertGreaterThan( $t2, $t3, 'Check key time increased' );
-
-               $t4 = $cache->getCheckKeyTime( $key );
-               $this->assertEquals( $t3, $t4, 'Check key time did not change' );
-
-               $mockWallClock += 0.100;
-               $cache->resetCheckKey( $key );
-               $t5 = $cache->getCheckKeyTime( $key );
-               $this->assertGreaterThan( $t4, $t5, 'Check key time increased' );
-
-               $t6 = $cache->getCheckKeyTime( $key );
-               $this->assertEquals( $t5, $t6, 'Check key time did not change' );
-       }
-
-       /**
-        * @covers WANObjectCache::getMulti()
-        */
-       public function testGetWithSeveralCheckKeys() {
-               $key = wfRandomString();
-               $tKey1 = wfRandomString();
-               $tKey2 = wfRandomString();
-               $value = 'meow';
-
-               $mockWallClock = 1549343530.2053;
-               $priorTime = $mockWallClock; // reference time
-               $this->cache->setMockTime( $mockWallClock );
-
-               // Two check keys are newer (given hold-off) than $key, another is older
-               $this->internalCache->set(
-                       WANObjectCache::TIME_KEY_PREFIX . $tKey2,
-                       WANObjectCache::PURGE_VAL_PREFIX . ( $priorTime - 3 )
-               );
-               $this->internalCache->set(
-                       WANObjectCache::TIME_KEY_PREFIX . $tKey2,
-                       WANObjectCache::PURGE_VAL_PREFIX . ( $priorTime - 5 )
-               );
-               $this->internalCache->set(
-                       WANObjectCache::TIME_KEY_PREFIX . $tKey1,
-                       WANObjectCache::PURGE_VAL_PREFIX . ( $priorTime - 30 )
-               );
-               $this->cache->set( $key, $value, 30 );
-
-               $curTTL = null;
-               $v = $this->cache->get( $key, $curTTL, [ $tKey1, $tKey2 ] );
-               $this->assertEquals( $value, $v, "Value matches" );
-               $this->assertLessThan( -4.9, $curTTL, "Correct CTL" );
-               $this->assertGreaterThan( -5.1, $curTTL, "Correct CTL" );
-       }
-
-       /**
-        * @covers WANObjectCache::reap()
-        * @covers WANObjectCache::reapCheckKey()
-        */
-       public function testReap() {
-               $vKey1 = wfRandomString();
-               $vKey2 = wfRandomString();
-               $tKey1 = wfRandomString();
-               $tKey2 = wfRandomString();
-               $value = 'moo';
-
-               $knownPurge = time() - 60;
-               $goodTime = microtime( true ) - 5;
-               $badTime = microtime( true ) - 300;
-
-               $this->internalCache->set(
-                       WANObjectCache::VALUE_KEY_PREFIX . $vKey1,
-                       [
-                               WANObjectCache::FLD_VERSION => WANObjectCache::VERSION,
-                               WANObjectCache::FLD_VALUE => $value,
-                               WANObjectCache::FLD_TTL => 3600,
-                               WANObjectCache::FLD_TIME => $goodTime
-                       ]
-               );
-               $this->internalCache->set(
-                       WANObjectCache::VALUE_KEY_PREFIX . $vKey2,
-                       [
-                               WANObjectCache::FLD_VERSION => WANObjectCache::VERSION,
-                               WANObjectCache::FLD_VALUE => $value,
-                               WANObjectCache::FLD_TTL => 3600,
-                               WANObjectCache::FLD_TIME => $badTime
-                       ]
-               );
-               $this->internalCache->set(
-                       WANObjectCache::TIME_KEY_PREFIX . $tKey1,
-                       WANObjectCache::PURGE_VAL_PREFIX . $goodTime
-               );
-               $this->internalCache->set(
-                       WANObjectCache::TIME_KEY_PREFIX . $tKey2,
-                       WANObjectCache::PURGE_VAL_PREFIX . $badTime
-               );
-
-               $this->assertEquals( $value, $this->cache->get( $vKey1 ) );
-               $this->assertEquals( $value, $this->cache->get( $vKey2 ) );
-               $this->cache->reap( $vKey1, $knownPurge, $bad1 );
-               $this->cache->reap( $vKey2, $knownPurge, $bad2 );
-
-               $this->assertFalse( $bad1 );
-               $this->assertTrue( $bad2 );
-
-               $this->cache->reapCheckKey( $tKey1, $knownPurge, $tBad1 );
-               $this->cache->reapCheckKey( $tKey2, $knownPurge, $tBad2 );
-               $this->assertFalse( $tBad1 );
-               $this->assertTrue( $tBad2 );
-       }
-
-       /**
-        * @covers WANObjectCache::reap()
-        */
-       public function testReap_fail() {
-               $backend = $this->getMockBuilder( EmptyBagOStuff::class )
-                       ->setMethods( [ 'get', 'changeTTL' ] )->getMock();
-               $backend->expects( $this->once() )->method( 'get' )
-                       ->willReturn( [
-                               WANObjectCache::FLD_VERSION => WANObjectCache::VERSION,
-                               WANObjectCache::FLD_VALUE => 'value',
-                               WANObjectCache::FLD_TTL => 3600,
-                               WANObjectCache::FLD_TIME => 300,
-                       ] );
-               $backend->expects( $this->once() )->method( 'changeTTL' )
-                       ->willReturn( false );
-
-               $wanCache = new WANObjectCache( [
-                       'cache' => $backend
-               ] );
-
-               $isStale = null;
-               $ret = $wanCache->reap( 'key', 360, $isStale );
-               $this->assertTrue( $isStale, 'value was stale' );
-               $this->assertFalse( $ret, 'changeTTL failed' );
-       }
-
-       /**
-        * @covers WANObjectCache::set()
-        */
-       public function testSetWithLag() {
-               $value = 1;
-
-               $key = wfRandomString();
-               $opts = [ 'lag' => 300, 'since' => microtime( true ) ];
-               $this->cache->set( $key, $value, 30, $opts );
-               $this->assertEquals( $value, $this->cache->get( $key ), "Rep-lagged value written." );
-
-               $key = wfRandomString();
-               $opts = [ 'lag' => 0, 'since' => microtime( true ) - 300 ];
-               $this->cache->set( $key, $value, 30, $opts );
-               $this->assertEquals( false, $this->cache->get( $key ), "Trx-lagged value not written." );
-
-               $key = wfRandomString();
-               $opts = [ 'lag' => 5, 'since' => microtime( true ) - 5 ];
-               $this->cache->set( $key, $value, 30, $opts );
-               $this->assertEquals( false, $this->cache->get( $key ), "Lagged value not written." );
-       }
-
-       /**
-        * @covers WANObjectCache::set()
-        */
-       public function testWritePending() {
-               $value = 1;
-
-               $key = wfRandomString();
-               $opts = [ 'pending' => true ];
-               $this->cache->set( $key, $value, 30, $opts );
-               $this->assertEquals( false, $this->cache->get( $key ), "Pending value not written." );
-       }
-
-       public function testMcRouterSupport() {
-               $localBag = $this->getMockBuilder( EmptyBagOStuff::class )
-                       ->setMethods( [ 'set', 'delete' ] )->getMock();
-               $localBag->expects( $this->never() )->method( 'set' );
-               $localBag->expects( $this->never() )->method( 'delete' );
-               $wanCache = new WANObjectCache( [
-                       'cache' => $localBag,
-                       'mcrouterAware' => true,
-                       'region' => 'pmtpa',
-                       'cluster' => 'mw-wan'
-               ] );
-               $valFunc = function () {
-                       return 1;
-               };
-
-               // None of these should use broadcasting commands (e.g. SET, DELETE)
-               $wanCache->get( 'x' );
-               $wanCache->get( 'x', $ctl, [ 'check1' ] );
-               $wanCache->getMulti( [ 'x', 'y' ] );
-               $wanCache->getMulti( [ 'x', 'y' ], $ctls, [ 'check2' ] );
-               $wanCache->getWithSetCallback( 'p', 30, $valFunc );
-               $wanCache->getCheckKeyTime( 'zzz' );
-               $wanCache->reap( 'x', time() - 300 );
-               $wanCache->reap( 'zzz', time() - 300 );
-       }
-
-       public function testMcRouterSupportBroadcastDelete() {
-               $localBag = $this->getMockBuilder( EmptyBagOStuff::class )
-                       ->setMethods( [ 'set' ] )->getMock();
-               $wanCache = new WANObjectCache( [
-                       'cache' => $localBag,
-                       'mcrouterAware' => true,
-                       'region' => 'pmtpa',
-                       'cluster' => 'mw-wan'
-               ] );
-
-               $localBag->expects( $this->once() )->method( 'set' )
-                       ->with( "/*/mw-wan/" . $wanCache::VALUE_KEY_PREFIX . "test" );
-
-               $wanCache->delete( 'test' );
-       }
-
-       public function testMcRouterSupportBroadcastTouchCK() {
-               $localBag = $this->getMockBuilder( EmptyBagOStuff::class )
-                       ->setMethods( [ 'set' ] )->getMock();
-               $wanCache = new WANObjectCache( [
-                       'cache' => $localBag,
-                       'mcrouterAware' => true,
-                       'region' => 'pmtpa',
-                       'cluster' => 'mw-wan'
-               ] );
-
-               $localBag->expects( $this->once() )->method( 'set' )
-                       ->with( "/*/mw-wan/" . $wanCache::TIME_KEY_PREFIX . "test" );
-
-               $wanCache->touchCheckKey( 'test' );
-       }
-
-       public function testMcRouterSupportBroadcastResetCK() {
-               $localBag = $this->getMockBuilder( EmptyBagOStuff::class )
-                       ->setMethods( [ 'delete' ] )->getMock();
-               $wanCache = new WANObjectCache( [
-                       'cache' => $localBag,
-                       'mcrouterAware' => true,
-                       'region' => 'pmtpa',
-                       'cluster' => 'mw-wan'
-               ] );
-
-               $localBag->expects( $this->once() )->method( 'delete' )
-                       ->with( "/*/mw-wan/" . $wanCache::TIME_KEY_PREFIX . "test" );
-
-               $wanCache->resetCheckKey( 'test' );
-       }
-
-       public function testEpoch() {
-               $bag = new HashBagOStuff();
-               $cache = new WANObjectCache( [ 'cache' => $bag ] );
-               $key = $cache->makeGlobalKey( 'The whole of the Law' );
-
-               $now = microtime( true );
-               $cache->setMockTime( $now );
-
-               $cache->set( $key, 'Do what thou Wilt' );
-               $cache->touchCheckKey( $key );
-
-               $then = $now;
-               $now += 30;
-               $this->assertEquals( 'Do what thou Wilt', $cache->get( $key ) );
-               $this->assertEquals( $then, $cache->getCheckKeyTime( $key ), 'Check key init', 0.01 );
-
-               $cache = new WANObjectCache( [
-                       'cache' => $bag,
-                       'epoch' => $now - 3600
-               ] );
-               $cache->setMockTime( $now );
-
-               $this->assertEquals( 'Do what thou Wilt', $cache->get( $key ) );
-               $this->assertEquals( $then, $cache->getCheckKeyTime( $key ), 'Check key kept', 0.01 );
-
-               $now += 30;
-               $cache = new WANObjectCache( [
-                       'cache' => $bag,
-                       'epoch' => $now + 3600
-               ] );
-               $cache->setMockTime( $now );
-
-               $this->assertFalse( $cache->get( $key ), 'Key rejected due to epoch' );
-               $this->assertEquals( $now, $cache->getCheckKeyTime( $key ), 'Check key reset', 0.01 );
-       }
-
-       /**
-        * @dataProvider provideAdaptiveTTL
-        * @covers WANObjectCache::adaptiveTTL()
-        * @param float|int $ago
-        * @param int $maxTTL
-        * @param int $minTTL
-        * @param float $factor
-        * @param int $adaptiveTTL
-        */
-       public function testAdaptiveTTL( $ago, $maxTTL, $minTTL, $factor, $adaptiveTTL ) {
-               $mtime = $ago ? time() - $ago : $ago;
-               $margin = 5;
-               $ttl = $this->cache->adaptiveTTL( $mtime, $maxTTL, $minTTL, $factor );
-
-               $this->assertGreaterThanOrEqual( $adaptiveTTL - $margin, $ttl );
-               $this->assertLessThanOrEqual( $adaptiveTTL + $margin, $ttl );
-
-               $ttl = $this->cache->adaptiveTTL( (string)$mtime, $maxTTL, $minTTL, $factor );
-
-               $this->assertGreaterThanOrEqual( $adaptiveTTL - $margin, $ttl );
-               $this->assertLessThanOrEqual( $adaptiveTTL + $margin, $ttl );
-       }
-
-       public static function provideAdaptiveTTL() {
-               return [
-                       [ 3600, 900, 30, 0.2, 720 ],
-                       [ 3600, 500, 30, 0.2, 500 ],
-                       [ 3600, 86400, 800, 0.2, 800 ],
-                       [ false, 86400, 800, 0.2, 800 ],
-                       [ null, 86400, 800, 0.2, 800 ]
-               ];
-       }
-
-       /**
-        * @covers WANObjectCache::__construct
-        * @covers WANObjectCache::newEmpty
-        */
-       public function testNewEmpty() {
-               $this->assertInstanceOf(
-                       WANObjectCache::class,
-                       WANObjectCache::newEmpty()
-               );
-       }
-
-       /**
-        * @covers WANObjectCache::setLogger
-        */
-       public function testSetLogger() {
-               $this->assertSame( null, $this->cache->setLogger( new Psr\Log\NullLogger ) );
-       }
-
-       /**
-        * @covers WANObjectCache::getQoS
-        */
-       public function testGetQoS() {
-               $backend = $this->getMockBuilder( HashBagOStuff::class )
-                       ->setMethods( [ 'getQoS' ] )->getMock();
-               $backend->expects( $this->once() )->method( 'getQoS' )
-                       ->willReturn( BagOStuff::QOS_UNKNOWN );
-               $wanCache = new WANObjectCache( [ 'cache' => $backend ] );
-
-               $this->assertSame(
-                       $wanCache::QOS_UNKNOWN,
-                       $wanCache->getQoS( $wanCache::ATTR_EMULATION )
-               );
-       }
-
-       /**
-        * @covers WANObjectCache::makeKey
-        */
-       public function testMakeKey() {
-               $backend = $this->getMockBuilder( HashBagOStuff::class )
-                       ->setMethods( [ 'makeKey' ] )->getMock();
-               $backend->expects( $this->once() )->method( 'makeKey' )
-                       ->willReturn( 'special' );
-
-               $wanCache = new WANObjectCache( [
-                       'cache' => $backend
-               ] );
-
-               $this->assertSame( 'special', $wanCache->makeKey( 'a', 'b' ) );
-       }
-
-       /**
-        * @covers WANObjectCache::makeGlobalKey
-        */
-       public function testMakeGlobalKey() {
-               $backend = $this->getMockBuilder( HashBagOStuff::class )
-                       ->setMethods( [ 'makeGlobalKey' ] )->getMock();
-               $backend->expects( $this->once() )->method( 'makeGlobalKey' )
-                       ->willReturn( 'special' );
-
-               $wanCache = new WANObjectCache( [
-                       'cache' => $backend
-               ] );
-
-               $this->assertSame( 'special', $wanCache->makeGlobalKey( 'a', 'b' ) );
-       }
-
-       public static function statsKeyProvider() {
-               return [
-                       [ 'domain:page:5', 'page' ],
-                       [ 'domain:main-key', 'main-key' ],
-                       [ 'domain:page:history', 'page' ],
-                       [ 'missingdomainkey', 'missingdomainkey' ]
-               ];
-       }
-
-       /**
-        * @dataProvider statsKeyProvider
-        * @covers WANObjectCache::determineKeyClassForStats
-        */
-       public function testStatsKeyClass( $key, $class ) {
-               $wanCache = TestingAccessWrapper::newFromObject( new WANObjectCache( [
-                       'cache' => new HashBagOStuff
-               ] ) );
-
-               $this->assertEquals( $class, $wanCache->determineKeyClassForStats( $key ) );
-       }
-}
-
-class NearExpiringWANObjectCache extends WANObjectCache {
-       const CLOCK_SKEW = 1;
-
-       protected function worthRefreshExpiring( $curTTL, $lowTTL ) {
-               return ( $curTTL > 0 && ( $curTTL + self::CLOCK_SKEW ) < $lowTTL );
-       }
-}
-
-class PopularityRefreshingWANObjectCache extends WANObjectCache {
-       protected function worthRefreshPopular( $asOf, $ageNew, $timeTillRefresh, $now ) {
-               return ( ( $now - $asOf ) > $timeTillRefresh );
-       }
-}
diff --git a/tests/phpunit/includes/libs/rdbms/ChronologyProtectorTest.php b/tests/phpunit/includes/libs/rdbms/ChronologyProtectorTest.php
deleted file mode 100644 (file)
index 5901bc1..0000000
+++ /dev/null
@@ -1,81 +0,0 @@
-<?php
-
-/**
- * Holds tests for ChronologyProtector abstract MediaWiki class.
- *
- * 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; either version 2 of the License, or
- * (at your option) any later version.
- *
- * 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.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- */
-
-use Wikimedia\Rdbms\ChronologyProtector;
-
-/**
- * @group Database
- * @covers \Wikimedia\Rdbms\ChronologyProtector::__construct
- * @covers \Wikimedia\Rdbms\ChronologyProtector::getClientId
- */
-class ChronologyProtectorTest extends PHPUnit\Framework\TestCase {
-       /**
-        * @dataProvider clientIdProvider
-        * @param array $client
-        * @param string $secret
-        * @param string $expectedId
-        */
-       public function testClientId( array $client, $secret, $expectedId ) {
-               $bag = new HashBagOStuff();
-               $cp = new ChronologyProtector( $bag, $client, null, $secret );
-
-               $this->assertEquals( $expectedId, $cp->getClientId() );
-       }
-
-       public function clientIdProvider() {
-               return [
-                       [
-                               [
-                                       'ip' => '127.0.0.1',
-                                       'agent' => "Totally-Not-FireFox"
-                               ],
-                               '',
-                               '45e93a9c215c031d38b7c42d8e4700ca',
-                       ],
-                       [
-                               [
-                                       'ip' => '127.0.0.7',
-                                       'agent' => "Totally-Not-FireFox"
-                               ],
-                               '',
-                               'b1d604117b51746c35c3df9f293c84dc'
-                       ],
-                       [
-                               [
-                                       'ip' => '127.0.0.1',
-                                       'agent' => "Totally-FireFox"
-                               ],
-                               '',
-                               '731b4e06a65e2346b497fc811571c4d7'
-                       ],
-                       [
-                               [
-                                       'ip' => '127.0.0.1',
-                                       'agent' => "Totally-Not-FireFox"
-                               ],
-                               'secret',
-                               'defff51ded73cd901253d874c9b2077d'
-                       ]
-               ];
-       }
-}
diff --git a/tests/phpunit/includes/libs/rdbms/TransactionProfilerTest.php b/tests/phpunit/includes/libs/rdbms/TransactionProfilerTest.php
deleted file mode 100644 (file)
index 538d625..0000000
+++ /dev/null
@@ -1,147 +0,0 @@
-<?php
-
-use Wikimedia\Rdbms\TransactionProfiler;
-use Psr\Log\LoggerInterface;
-
-/**
- * @covers \Wikimedia\Rdbms\TransactionProfiler
- */
-class TransactionProfilerTest extends PHPUnit\Framework\TestCase {
-
-       use MediaWikiCoversValidator;
-
-       public function testAffected() {
-               $logger = $this->getMockBuilder( LoggerInterface::class )->getMock();
-               $logger->expects( $this->exactly( 3 ) )->method( 'warning' );
-
-               $tp = new TransactionProfiler();
-               $tp->setLogger( $logger );
-               $tp->setExpectation( 'maxAffected', 100, __METHOD__ );
-
-               $tp->transactionWritingIn( 'srv1', 'db1', '123' );
-               $tp->recordQueryCompletion( "SQL 1", microtime( true ) - 3, true, 200 );
-               $tp->recordQueryCompletion( "SQL 2", microtime( true ) - 3, true, 200 );
-               $tp->transactionWritingOut( 'srv1', 'db1', '123', 1, 400 );
-       }
-
-       public function testReadTime() {
-               $logger = $this->getMockBuilder( LoggerInterface::class )->getMock();
-               // 1 per query
-               $logger->expects( $this->exactly( 2 ) )->method( 'warning' );
-
-               $tp = new TransactionProfiler();
-               $tp->setLogger( $logger );
-               $tp->setExpectation( 'readQueryTime', 5, __METHOD__ );
-
-               $tp->transactionWritingIn( 'srv1', 'db1', '123' );
-               $tp->recordQueryCompletion( "SQL 1", microtime( true ) - 10, false, 1 );
-               $tp->recordQueryCompletion( "SQL 2", microtime( true ) - 10, false, 1 );
-               $tp->transactionWritingOut( 'srv1', 'db1', '123', 0, 0 );
-       }
-
-       public function testWriteTime() {
-               $logger = $this->getMockBuilder( LoggerInterface::class )->getMock();
-               // 1 per query, 1 per trx, and one "sub-optimal trx" entry
-               $logger->expects( $this->exactly( 4 ) )->method( 'warning' );
-
-               $tp = new TransactionProfiler();
-               $tp->setLogger( $logger );
-               $tp->setExpectation( 'writeQueryTime', 5, __METHOD__ );
-
-               $tp->transactionWritingIn( 'srv1', 'db1', '123' );
-               $tp->recordQueryCompletion( "SQL 1", microtime( true ) - 10, true, 1 );
-               $tp->recordQueryCompletion( "SQL 2", microtime( true ) - 10, true, 1 );
-               $tp->transactionWritingOut( 'srv1', 'db1', '123', 20, 1 );
-       }
-
-       public function testAffectedTrx() {
-               $logger = $this->getMockBuilder( LoggerInterface::class )->getMock();
-               $logger->expects( $this->exactly( 1 ) )->method( 'warning' );
-
-               $tp = new TransactionProfiler();
-               $tp->setLogger( $logger );
-               $tp->setExpectation( 'maxAffected', 100, __METHOD__ );
-
-               $tp->transactionWritingIn( 'srv1', 'db1', '123' );
-               $tp->transactionWritingOut( 'srv1', 'db1', '123', 1, 200 );
-       }
-
-       public function testWriteTimeTrx() {
-               $logger = $this->getMockBuilder( LoggerInterface::class )->getMock();
-               // 1 per trx, and one "sub-optimal trx" entry
-               $logger->expects( $this->exactly( 2 ) )->method( 'warning' );
-
-               $tp = new TransactionProfiler();
-               $tp->setLogger( $logger );
-               $tp->setExpectation( 'writeQueryTime', 5, __METHOD__ );
-
-               $tp->transactionWritingIn( 'srv1', 'db1', '123' );
-               $tp->transactionWritingOut( 'srv1', 'db1', '123', 10, 1 );
-       }
-
-       public function testConns() {
-               $logger = $this->getMockBuilder( LoggerInterface::class )->getMock();
-               $logger->expects( $this->exactly( 2 ) )->method( 'warning' );
-
-               $tp = new TransactionProfiler();
-               $tp->setLogger( $logger );
-               $tp->setExpectation( 'conns', 2, __METHOD__ );
-
-               $tp->recordConnection( 'srv1', 'db1', false );
-               $tp->recordConnection( 'srv1', 'db2', false );
-               $tp->recordConnection( 'srv1', 'db3', false ); // warn
-               $tp->recordConnection( 'srv1', 'db4', false ); // warn
-       }
-
-       public function testMasterConns() {
-               $logger = $this->getMockBuilder( LoggerInterface::class )->getMock();
-               $logger->expects( $this->exactly( 2 ) )->method( 'warning' );
-
-               $tp = new TransactionProfiler();
-               $tp->setLogger( $logger );
-               $tp->setExpectation( 'masterConns', 2, __METHOD__ );
-
-               $tp->recordConnection( 'srv1', 'db1', false );
-               $tp->recordConnection( 'srv1', 'db2', false );
-
-               $tp->recordConnection( 'srv1', 'db1', true );
-               $tp->recordConnection( 'srv1', 'db2', true );
-               $tp->recordConnection( 'srv1', 'db3', true ); // warn
-               $tp->recordConnection( 'srv1', 'db4', true ); // warn
-       }
-
-       public function testReadQueryCount() {
-               $logger = $this->getMockBuilder( LoggerInterface::class )->getMock();
-               $logger->expects( $this->exactly( 2 ) )->method( 'warning' );
-
-               $tp = new TransactionProfiler();
-               $tp->setLogger( $logger );
-               $tp->setExpectation( 'queries', 2, __METHOD__ );
-
-               $tp->recordQueryCompletion( "SQL 1", microtime( true ) - 0.01, false, 0 );
-               $tp->recordQueryCompletion( "SQL 2", microtime( true ) - 0.01, false, 0 );
-               $tp->recordQueryCompletion( "SQL 3", microtime( true ) - 0.01, false, 0 ); // warn
-               $tp->recordQueryCompletion( "SQL 4", microtime( true ) - 0.01, false, 0 ); // warn
-       }
-
-       public function testWriteQueryCount() {
-               $logger = $this->getMockBuilder( LoggerInterface::class )->getMock();
-               $logger->expects( $this->exactly( 2 ) )->method( 'warning' );
-
-               $tp = new TransactionProfiler();
-               $tp->setLogger( $logger );
-               $tp->setExpectation( 'writes', 2, __METHOD__ );
-
-               $tp->recordQueryCompletion( "SQL 1", microtime( true ) - 0.01, false, 0 );
-               $tp->recordQueryCompletion( "SQL 2", microtime( true ) - 0.01, false, 0 );
-               $tp->recordQueryCompletion( "SQL 3", microtime( true ) - 0.01, false, 0 );
-               $tp->recordQueryCompletion( "SQL 4", microtime( true ) - 0.01, false, 0 );
-
-               $tp->transactionWritingIn( 'srv1', 'db1', '123' );
-               $tp->recordQueryCompletion( "SQL 1w", microtime( true ) - 0.01, true, 2 );
-               $tp->recordQueryCompletion( "SQL 2w", microtime( true ) - 0.01, true, 5 );
-               $tp->recordQueryCompletion( "SQL 3w", microtime( true ) - 0.01, true, 3 );
-               $tp->recordQueryCompletion( "SQL 4w", microtime( true ) - 0.01, true, 1 );
-               $tp->transactionWritingOut( 'srv1', 'db1', '123', 1, 1 );
-       }
-}
diff --git a/tests/phpunit/includes/libs/rdbms/connectionmanager/ConnectionManagerTest.php b/tests/phpunit/includes/libs/rdbms/connectionmanager/ConnectionManagerTest.php
deleted file mode 100644 (file)
index dd86a73..0000000
+++ /dev/null
@@ -1,139 +0,0 @@
-<?php
-
-namespace Wikimedia\Tests\Rdbms;
-
-use Wikimedia\Rdbms\IDatabase;
-use Wikimedia\Rdbms\LoadBalancer;
-use PHPUnit_Framework_MockObject_MockObject;
-use Wikimedia\Rdbms\ConnectionManager;
-
-/**
- * @covers Wikimedia\Rdbms\ConnectionManager
- *
- * @author Daniel Kinzler
- */
-class ConnectionManagerTest extends \PHPUnit\Framework\TestCase {
-
-       /**
-        * @return IDatabase|PHPUnit_Framework_MockObject_MockObject
-        */
-       private function getIDatabaseMock() {
-               return $this->getMockBuilder( IDatabase::class )
-                       ->getMock();
-       }
-
-       /**
-        * @return LoadBalancer|PHPUnit_Framework_MockObject_MockObject
-        */
-       private function getLoadBalancerMock() {
-               $lb = $this->getMockBuilder( LoadBalancer::class )
-                       ->disableOriginalConstructor()
-                       ->getMock();
-
-               return $lb;
-       }
-
-       public function testGetReadConnection_nullGroups() {
-               $database = $this->getIDatabaseMock();
-               $lb = $this->getLoadBalancerMock();
-
-               $lb->expects( $this->once() )
-                       ->method( 'getConnection' )
-                       ->with( DB_REPLICA, [ 'group1' ], 'someDbName' )
-                       ->will( $this->returnValue( $database ) );
-
-               $manager = new ConnectionManager( $lb, 'someDbName', [ 'group1' ] );
-               $actual = $manager->getReadConnection();
-
-               $this->assertSame( $database, $actual );
-       }
-
-       public function testGetReadConnection_withGroups() {
-               $database = $this->getIDatabaseMock();
-               $lb = $this->getLoadBalancerMock();
-
-               $lb->expects( $this->once() )
-                       ->method( 'getConnection' )
-                       ->with( DB_REPLICA, [ 'group2' ], 'someDbName' )
-                       ->will( $this->returnValue( $database ) );
-
-               $manager = new ConnectionManager( $lb, 'someDbName', [ 'group1' ] );
-               $actual = $manager->getReadConnection( [ 'group2' ] );
-
-               $this->assertSame( $database, $actual );
-       }
-
-       public function testGetWriteConnection() {
-               $database = $this->getIDatabaseMock();
-               $lb = $this->getLoadBalancerMock();
-
-               $lb->expects( $this->once() )
-                       ->method( 'getConnection' )
-                       ->with( DB_MASTER, [ 'group1' ], 'someDbName' )
-                       ->will( $this->returnValue( $database ) );
-
-               $manager = new ConnectionManager( $lb, 'someDbName', [ 'group1' ] );
-               $actual = $manager->getWriteConnection();
-
-               $this->assertSame( $database, $actual );
-       }
-
-       public function testReleaseConnection() {
-               $database = $this->getIDatabaseMock();
-               $lb = $this->getLoadBalancerMock();
-
-               $lb->expects( $this->once() )
-                       ->method( 'reuseConnection' )
-                       ->with( $database )
-                       ->will( $this->returnValue( null ) );
-
-               $manager = new ConnectionManager( $lb );
-               $manager->releaseConnection( $database );
-       }
-
-       public function testGetReadConnectionRef_nullGroups() {
-               $database = $this->getIDatabaseMock();
-               $lb = $this->getLoadBalancerMock();
-
-               $lb->expects( $this->once() )
-                       ->method( 'getConnectionRef' )
-                       ->with( DB_REPLICA, [ 'group1' ], 'someDbName' )
-                       ->will( $this->returnValue( $database ) );
-
-               $manager = new ConnectionManager( $lb, 'someDbName', [ 'group1' ] );
-               $actual = $manager->getReadConnectionRef();
-
-               $this->assertSame( $database, $actual );
-       }
-
-       public function testGetReadConnectionRef_withGroups() {
-               $database = $this->getIDatabaseMock();
-               $lb = $this->getLoadBalancerMock();
-
-               $lb->expects( $this->once() )
-                       ->method( 'getConnectionRef' )
-                       ->with( DB_REPLICA, [ 'group2' ], 'someDbName' )
-                       ->will( $this->returnValue( $database ) );
-
-               $manager = new ConnectionManager( $lb, 'someDbName', [ 'group1' ] );
-               $actual = $manager->getReadConnectionRef( [ 'group2' ] );
-
-               $this->assertSame( $database, $actual );
-       }
-
-       public function testGetWriteConnectionRef() {
-               $database = $this->getIDatabaseMock();
-               $lb = $this->getLoadBalancerMock();
-
-               $lb->expects( $this->once() )
-                       ->method( 'getConnectionRef' )
-                       ->with( DB_MASTER, [ 'group1' ], 'someDbName' )
-                       ->will( $this->returnValue( $database ) );
-
-               $manager = new ConnectionManager( $lb, 'someDbName', [ 'group1' ] );
-               $actual = $manager->getWriteConnectionRef();
-
-               $this->assertSame( $database, $actual );
-       }
-
-}
diff --git a/tests/phpunit/includes/libs/rdbms/connectionmanager/SessionConsistentConnectionManagerTest.php b/tests/phpunit/includes/libs/rdbms/connectionmanager/SessionConsistentConnectionManagerTest.php
deleted file mode 100644 (file)
index 8d7d104..0000000
+++ /dev/null
@@ -1,108 +0,0 @@
-<?php
-
-namespace Wikimedia\Tests\Rdbms;
-
-use Wikimedia\Rdbms\IDatabase;
-use Wikimedia\Rdbms\LoadBalancer;
-use PHPUnit_Framework_MockObject_MockObject;
-use Wikimedia\Rdbms\SessionConsistentConnectionManager;
-
-/**
- * @covers Wikimedia\Rdbms\SessionConsistentConnectionManager
- *
- * @author Daniel Kinzler
- */
-class SessionConsistentConnectionManagerTest extends \PHPUnit\Framework\TestCase {
-
-       /**
-        * @return IDatabase|PHPUnit_Framework_MockObject_MockObject
-        */
-       private function getIDatabaseMock() {
-               return $this->getMockBuilder( IDatabase::class )
-                       ->getMock();
-       }
-
-       /**
-        * @return LoadBalancer|PHPUnit_Framework_MockObject_MockObject
-        */
-       private function getLoadBalancerMock() {
-               $lb = $this->getMockBuilder( LoadBalancer::class )
-                       ->disableOriginalConstructor()
-                       ->getMock();
-
-               return $lb;
-       }
-
-       public function testGetReadConnection() {
-               $database = $this->getIDatabaseMock();
-               $lb = $this->getLoadBalancerMock();
-
-               $lb->expects( $this->once() )
-                       ->method( 'getConnection' )
-                       ->with( DB_REPLICA )
-                       ->will( $this->returnValue( $database ) );
-
-               $manager = new SessionConsistentConnectionManager( $lb );
-               $actual = $manager->getReadConnection();
-
-               $this->assertSame( $database, $actual );
-       }
-
-       public function testGetReadConnectionReturnsWriteDbOnForceMatser() {
-               $database = $this->getIDatabaseMock();
-               $lb = $this->getLoadBalancerMock();
-
-               $lb->expects( $this->once() )
-                       ->method( 'getConnection' )
-                       ->with( DB_MASTER )
-                       ->will( $this->returnValue( $database ) );
-
-               $manager = new SessionConsistentConnectionManager( $lb );
-               $manager->prepareForUpdates();
-               $actual = $manager->getReadConnection();
-
-               $this->assertSame( $database, $actual );
-       }
-
-       public function testGetWriteConnection() {
-               $database = $this->getIDatabaseMock();
-               $lb = $this->getLoadBalancerMock();
-
-               $lb->expects( $this->once() )
-                       ->method( 'getConnection' )
-                       ->with( DB_MASTER )
-                       ->will( $this->returnValue( $database ) );
-
-               $manager = new SessionConsistentConnectionManager( $lb );
-               $actual = $manager->getWriteConnection();
-
-               $this->assertSame( $database, $actual );
-       }
-
-       public function testForceMaster() {
-               $database = $this->getIDatabaseMock();
-               $lb = $this->getLoadBalancerMock();
-
-               $lb->expects( $this->once() )
-                       ->method( 'getConnection' )
-                       ->with( DB_MASTER )
-                       ->will( $this->returnValue( $database ) );
-
-               $manager = new SessionConsistentConnectionManager( $lb );
-               $manager->prepareForUpdates();
-               $manager->getReadConnection();
-       }
-
-       public function testReleaseConnection() {
-               $database = $this->getIDatabaseMock();
-               $lb = $this->getLoadBalancerMock();
-
-               $lb->expects( $this->once() )
-                       ->method( 'reuseConnection' )
-                       ->with( $database )
-                       ->will( $this->returnValue( null ) );
-
-               $manager = new SessionConsistentConnectionManager( $lb );
-               $manager->releaseConnection( $database );
-       }
-}
diff --git a/tests/phpunit/includes/libs/rdbms/database/DBConnRefTest.php b/tests/phpunit/includes/libs/rdbms/database/DBConnRefTest.php
deleted file mode 100644 (file)
index 33e5c3b..0000000
+++ /dev/null
@@ -1,223 +0,0 @@
-<?php
-
-use Wikimedia\Rdbms\Database;
-use Wikimedia\Rdbms\DBConnRef;
-use Wikimedia\Rdbms\FakeResultWrapper;
-use Wikimedia\Rdbms\IDatabase;
-use Wikimedia\Rdbms\ILoadBalancer;
-use Wikimedia\Rdbms\ResultWrapper;
-
-/**
- * @covers Wikimedia\Rdbms\DBConnRef
- */
-class DBConnRefTest extends PHPUnit\Framework\TestCase {
-
-       use MediaWikiCoversValidator;
-       use PHPUnit4And6Compat;
-
-       /**
-        * @return ILoadBalancer
-        */
-       private function getLoadBalancerMock() {
-               $lb = $this->getMock( ILoadBalancer::class );
-
-               $lb->method( 'getConnection' )->willReturnCallback(
-                       function () {
-                               return $this->getDatabaseMock();
-                       }
-               );
-
-               $lb->method( 'getConnectionRef' )->willReturnCallback(
-                       function () use ( $lb ) {
-                               return $this->getDBConnRef( $lb );
-                       }
-               );
-
-               return $lb;
-       }
-
-       /**
-        * @return IDatabase
-        */
-       private function getDatabaseMock() {
-               $db = $this->getMockBuilder( Database::class )
-                       ->disableOriginalConstructor()
-                       ->getMock();
-
-               $open = true;
-               $db->method( 'select' )->willReturnCallback( function () use ( &$open ) {
-                       if ( !$open ) {
-                               throw new LogicException( "Not open" );
-                       }
-
-                       return new FakeResultWrapper( [] );
-               } );
-               $db->method( 'close' )->willReturnCallback( function () use ( &$open ) {
-                       $open = false;
-
-                       return true;
-               } );
-               $db->method( 'isOpen' )->willReturnCallback( function () use ( &$open ) {
-                       return $open;
-               } );
-               $db->method( 'open' )->willReturnCallback( function () use ( &$open ) {
-                       $open = true;
-
-                       return $open;
-               } );
-               $db->method( '__toString' )->willReturn( 'MOCK_DB' );
-
-               return $db;
-       }
-
-       /**
-        * @return IDatabase
-        */
-       private function getDBConnRef( ILoadBalancer $lb = null ) {
-               $lb = $lb ?: $this->getLoadBalancerMock();
-               return new DBConnRef( $lb, $this->getDatabaseMock(), DB_MASTER );
-       }
-
-       public function testConstruct() {
-               $lb = $this->getLoadBalancerMock();
-               $ref = new DBConnRef( $lb, $this->getDatabaseMock(), DB_MASTER );
-
-               $this->assertInstanceOf( ResultWrapper::class, $ref->select( 'whatever', '*' ) );
-       }
-
-       public function testConstruct_params() {
-               $lb = $this->getMock( ILoadBalancer::class );
-
-               $lb->expects( $this->once() )
-                       ->method( 'getConnection' )
-                       ->with( DB_MASTER, [ 'test' ], 'dummy', ILoadBalancer::CONN_TRX_AUTOCOMMIT )
-                       ->willReturnCallback(
-                               function () {
-                                       return $this->getDatabaseMock();
-                               }
-                       );
-
-               $ref = new DBConnRef(
-                       $lb,
-                       [ DB_MASTER, [ 'test' ], 'dummy', ILoadBalancer::CONN_TRX_AUTOCOMMIT ],
-                       DB_MASTER
-               );
-
-               $this->assertInstanceOf( ResultWrapper::class, $ref->select( 'whatever', '*' ) );
-               $this->assertEquals( DB_MASTER, $ref->getReferenceRole() );
-
-               $ref2 = new DBConnRef(
-                       $lb,
-                       [ DB_MASTER, [ 'test' ], 'dummy', ILoadBalancer::CONN_TRX_AUTOCOMMIT ],
-                       DB_REPLICA
-               );
-               $this->assertEquals( DB_REPLICA, $ref2->getReferenceRole() );
-       }
-
-       public function testDestruct() {
-               $lb = $this->getLoadBalancerMock();
-
-               $lb->expects( $this->once() )
-                       ->method( 'reuseConnection' );
-
-               $this->innerMethodForTestDestruct( $lb );
-       }
-
-       private function innerMethodForTestDestruct( ILoadBalancer $lb ) {
-               $ref = $lb->getConnectionRef( DB_REPLICA );
-
-               $this->assertInstanceOf( ResultWrapper::class, $ref->select( 'whatever', '*' ) );
-       }
-
-       public function testConstruct_failure() {
-               $this->setExpectedException( InvalidArgumentException::class, '' );
-
-               $lb = $this->getLoadBalancerMock();
-               new DBConnRef( $lb, 17, DB_REPLICA ); // bad constructor argument
-       }
-
-       /**
-        * @covers Wikimedia\Rdbms\DBConnRef::getDomainId
-        */
-       public function testGetDomainID() {
-               $lb = $this->getMock( ILoadBalancer::class );
-
-               // getDomainID is optimized to not create a connection
-               $lb->expects( $this->never() )
-                       ->method( 'getConnection' );
-
-               $ref = new DBConnRef( $lb, [ DB_REPLICA, [], 'dummy', 0 ], DB_REPLICA );
-
-               $this->assertSame( 'dummy', $ref->getDomainID() );
-       }
-
-       /**
-        * @covers Wikimedia\Rdbms\DBConnRef::select
-        */
-       public function testSelect() {
-               // select should get passed through normally
-               $ref = $this->getDBConnRef();
-               $this->assertInstanceOf( ResultWrapper::class, $ref->select( 'whatever', '*' ) );
-       }
-
-       public function testToString() {
-               $ref = $this->getDBConnRef();
-               $this->assertInternalType( 'string', $ref->__toString() );
-
-               $lb = $this->getLoadBalancerMock();
-               $ref = new DBConnRef( $lb, [ DB_MASTER, [], 'test', 0 ], DB_MASTER );
-               $this->assertInternalType( 'string', $ref->__toString() );
-       }
-
-       /**
-        * @covers Wikimedia\Rdbms\DBConnRef::close
-        * @expectedException \Wikimedia\Rdbms\DBUnexpectedError
-        */
-       public function testClose() {
-               $lb = $this->getLoadBalancerMock();
-               $ref = new DBConnRef( $lb, [ DB_REPLICA, [], 'dummy', 0 ], DB_MASTER );
-               $ref->close();
-       }
-
-       /**
-        * @covers Wikimedia\Rdbms\DBConnRef::getReferenceRole
-        */
-       public function testGetReferenceRole() {
-               $lb = $this->getLoadBalancerMock();
-               $ref = new DBConnRef( $lb, [ DB_REPLICA, [], 'dummy', 0 ], DB_REPLICA );
-               $this->assertSame( DB_REPLICA, $ref->getReferenceRole() );
-
-               $ref = new DBConnRef( $lb, [ DB_MASTER, [], 'dummy', 0 ], DB_MASTER );
-               $this->assertSame( DB_MASTER, $ref->getReferenceRole() );
-
-               $ref = new DBConnRef( $lb, [ 1, [], 'dummy', 0 ], DB_REPLICA );
-               $this->assertSame( DB_REPLICA, $ref->getReferenceRole() );
-
-               $ref = new DBConnRef( $lb, [ 0, [], 'dummy', 0 ], DB_MASTER );
-               $this->assertSame( DB_MASTER, $ref->getReferenceRole() );
-       }
-
-       /**
-        * @covers Wikimedia\Rdbms\DBConnRef::getReferenceRole
-        * @expectedException Wikimedia\Rdbms\DBReadOnlyRoleError
-        * @dataProvider provideRoleExceptions
-        */
-       public function testRoleExceptions( $method, $args ) {
-               $lb = $this->getLoadBalancerMock();
-               $ref = new DBConnRef( $lb, [ DB_REPLICA, [], 'dummy', 0 ], DB_REPLICA );
-               $ref->$method( ...$args );
-       }
-
-       function provideRoleExceptions() {
-               return [
-                       [ 'insert', [ 'table', [ 'a' => 1 ] ] ],
-                       [ 'update', [ 'table', [ 'a' => 1 ], [ 'a' => 2 ] ] ],
-                       [ 'delete', [ 'table', [ 'a' => 1 ] ] ],
-                       [ 'replace', [ 'table', [ 'a' ], [ 'a' => 1 ] ] ],
-                       [ 'upsert', [ 'table', [ 'a' => 1 ], [ 'a' ], [ 'a = a + 1' ] ] ],
-                       [ 'lock', [ 'k', 'method' ] ],
-                       [ 'unlock', [ 'k', 'method' ] ],
-                       [ 'getScopedLockAndFlush', [ 'k', 'method', 1 ] ]
-               ];
-       }
-}
diff --git a/tests/phpunit/includes/libs/rdbms/database/DatabaseDomainTest.php b/tests/phpunit/includes/libs/rdbms/database/DatabaseDomainTest.php
deleted file mode 100644 (file)
index b1d4fad..0000000
+++ /dev/null
@@ -1,226 +0,0 @@
-<?php
-
-use Wikimedia\Rdbms\DatabaseDomain;
-
-/**
- * @covers Wikimedia\Rdbms\DatabaseDomain
- */
-class DatabaseDomainTest extends PHPUnit\Framework\TestCase {
-
-       use MediaWikiCoversValidator;
-       use PHPUnit4And6Compat;
-
-       public static function provideConstruct() {
-               return [
-                       'All strings' =>
-                               [ 'foo', 'bar', 'baz_', 'foo-bar-baz_' ],
-                       'Nothing' =>
-                               [ null, null, '', '' ],
-                       'Invalid $database' =>
-                               [ 0, 'bar', '', '', true ],
-                       'Invalid $schema' =>
-                               [ 'foo', 0, '', '', true ],
-                       'Invalid $prefix' =>
-                               [ 'foo', 'bar', 0, '', true ],
-                       'Dash' =>
-                               [ 'foo-bar', 'baz', 'baa_', 'foo?hbar-baz-baa_' ],
-                       'Question mark' =>
-                               [ 'foo?bar', 'baz', 'baa_', 'foo??bar-baz-baa_' ],
-               ];
-       }
-
-       /**
-        * @dataProvider provideConstruct
-        */
-       public function testConstruct( $db, $schema, $prefix, $id, $exception = false ) {
-               if ( $exception ) {
-                       $this->setExpectedException( InvalidArgumentException::class );
-                       new DatabaseDomain( $db, $schema, $prefix );
-                       return;
-               }
-
-               $domain = new DatabaseDomain( $db, $schema, $prefix );
-               $this->assertInstanceOf( DatabaseDomain::class, $domain );
-               $this->assertEquals( $db, $domain->getDatabase() );
-               $this->assertEquals( $schema, $domain->getSchema() );
-               $this->assertEquals( $prefix, $domain->getTablePrefix() );
-               $this->assertEquals( $id, $domain->getId() );
-               $this->assertEquals( $id, strval( $domain ), 'toString' );
-       }
-
-       public static function provideNewFromId() {
-               return [
-                       'Basic' =>
-                               [ 'foo', 'foo', null, '' ],
-                       'db+prefix' =>
-                               [ 'foo-bar_', 'foo', null, 'bar_' ],
-                       'db+schema+prefix' =>
-                               [ 'foo-bar-baz_', 'foo', 'bar', 'baz_' ],
-                       '?h -> -' =>
-                               [ 'foo?hbar-baz-baa_', 'foo-bar', 'baz', 'baa_' ],
-                       '?? -> ?' =>
-                               [ 'foo??bar-baz-baa_', 'foo?bar', 'baz', 'baa_' ],
-                       '? is left alone' =>
-                               [ 'foo?bar-baz-baa_', 'foo?bar', 'baz', 'baa_' ],
-                       'too many parts' =>
-                               [ 'foo-bar-baz-baa_', '', '', '', true ],
-                       'from instance' =>
-                               [ DatabaseDomain::newUnspecified(), null, null, '' ],
-               ];
-       }
-
-       /**
-        * @dataProvider provideNewFromId
-        */
-       public function testNewFromId( $id, $db, $schema, $prefix, $exception = false ) {
-               if ( $exception ) {
-                       $this->setExpectedException( InvalidArgumentException::class );
-                       DatabaseDomain::newFromId( $id );
-                       return;
-               }
-               $domain = DatabaseDomain::newFromId( $id );
-               $this->assertInstanceOf( DatabaseDomain::class, $domain );
-               $this->assertEquals( $db, $domain->getDatabase() );
-               $this->assertEquals( $schema, $domain->getSchema() );
-               $this->assertEquals( $prefix, $domain->getTablePrefix() );
-       }
-
-       public static function provideEquals() {
-               return [
-                       'Basic' =>
-                               [ 'foo', 'foo', null, '' ],
-                       'db+prefix' =>
-                               [ 'foo-bar_', 'foo', null, 'bar_' ],
-                       'db+schema+prefix' =>
-                               [ 'foo-bar-baz_', 'foo', 'bar', 'baz_' ],
-                       '?h -> -' =>
-                               [ 'foo?hbar-baz-baa_', 'foo-bar', 'baz', 'baa_' ],
-                       '?? -> ?' =>
-                               [ 'foo??bar-baz-baa_', 'foo?bar', 'baz', 'baa_' ],
-                       'Nothing' =>
-                               [ '', null, null, '' ],
-               ];
-       }
-
-       /**
-        * @dataProvider provideEquals
-        * @covers Wikimedia\Rdbms\DatabaseDomain::equals
-        */
-       public function testEquals( $id, $db, $schema, $prefix ) {
-               $fromId = DatabaseDomain::newFromId( $id );
-               $this->assertInstanceOf( DatabaseDomain::class, $fromId );
-
-               $constructed = new DatabaseDomain( $db, $schema, $prefix );
-
-               $this->assertTrue( $constructed->equals( $id ), 'constructed equals string' );
-               $this->assertTrue( $fromId->equals( $id ), 'fromId equals string' );
-
-               $this->assertTrue( $constructed->equals( $fromId ), 'compare constructed to newId' );
-               $this->assertTrue( $fromId->equals( $constructed ), 'compare newId to constructed' );
-       }
-
-       /**
-        * @covers Wikimedia\Rdbms\DatabaseDomain::newUnspecified
-        */
-       public function testNewUnspecified() {
-               $domain = DatabaseDomain::newUnspecified();
-               $this->assertInstanceOf( DatabaseDomain::class, $domain );
-               $this->assertTrue( $domain->equals( '' ) );
-               $this->assertSame( null, $domain->getDatabase() );
-               $this->assertSame( null, $domain->getSchema() );
-               $this->assertSame( '', $domain->getTablePrefix() );
-       }
-
-       public static function provideIsCompatible() {
-               return [
-                       'Basic' =>
-                               [ 'foo', 'foo', null, '', true ],
-                       'db+prefix' =>
-                               [ 'foo-bar_', 'foo', null, 'bar_', true ],
-                       'db+schema+prefix' =>
-                               [ 'foo-bar-baz_', 'foo', 'bar', 'baz_', true ],
-                       'db+dontcare_schema+prefix' =>
-                               [ 'foo-bar-baz_', 'foo', null, 'baz_', false ],
-                       '?h -> -' =>
-                               [ 'foo?hbar-baz-baa_', 'foo-bar', 'baz', 'baa_', true ],
-                       '?? -> ?' =>
-                               [ 'foo??bar-baz-baa_', 'foo?bar', 'baz', 'baa_', true ],
-                       'Nothing' =>
-                               [ '', null, null, '', true ],
-                       'dontcaredb+dontcaredbschema+prefix' =>
-                               [ 'mywiki-mediawiki-prefix_', null, null, 'prefix_', false ],
-                       'db+dontcareschema+prefix' =>
-                               [ 'mywiki-schema-prefix_', 'mywiki', null, 'prefix_', false ],
-                       'postgres-db-jobqueue' =>
-                               [ 'postgres-mediawiki-', 'postgres', null, '', false ]
-               ];
-       }
-
-       /**
-        * @dataProvider provideIsCompatible
-        * @covers Wikimedia\Rdbms\DatabaseDomain::isCompatible
-        */
-       public function testIsCompatible( $id, $db, $schema, $prefix, $transitive ) {
-               $compareIdObj = DatabaseDomain::newFromId( $id );
-               $this->assertInstanceOf( DatabaseDomain::class, $compareIdObj );
-
-               $fromId = new DatabaseDomain( $db, $schema, $prefix );
-
-               $this->assertTrue( $fromId->isCompatible( $id ), 'constructed equals string' );
-               $this->assertTrue( $fromId->isCompatible( $compareIdObj ), 'fromId equals string' );
-
-               $this->assertEquals( $transitive, $compareIdObj->isCompatible( $fromId ),
-                       'test transitivity of nulls components' );
-       }
-
-       public static function provideIsCompatible2() {
-               return [
-                       'db+schema+prefix' =>
-                               [ 'mywiki-schema-prefix_', 'thatwiki', 'schema', 'prefix_' ],
-                       'dontcaredb+dontcaredbschema+prefix' =>
-                               [ 'thatwiki-mediawiki-otherprefix_', null, null, 'prefix_' ],
-                       'db+dontcareschema+prefix' =>
-                               [ 'notmywiki-schema-prefix_', 'mywiki', null, 'prefix_' ],
-               ];
-       }
-
-       /**
-        * @dataProvider provideIsCompatible2
-        * @covers Wikimedia\Rdbms\DatabaseDomain::isCompatible
-        */
-       public function testIsCompatible2( $id, $db, $schema, $prefix ) {
-               $compareIdObj = DatabaseDomain::newFromId( $id );
-               $this->assertInstanceOf( DatabaseDomain::class, $compareIdObj );
-
-               $fromId = new DatabaseDomain( $db, $schema, $prefix );
-
-               $this->assertFalse( $fromId->isCompatible( $id ), 'constructed equals string' );
-               $this->assertFalse( $fromId->isCompatible( $compareIdObj ), 'fromId equals string' );
-       }
-
-       /**
-        * @expectedException InvalidArgumentException
-        */
-       public function testSchemaWithNoDB1() {
-               new DatabaseDomain( null, 'schema', '' );
-       }
-
-       /**
-        * @expectedException InvalidArgumentException
-        */
-       public function testSchemaWithNoDB2() {
-               DatabaseDomain::newFromId( '-schema-prefix' );
-       }
-
-       /**
-        * @covers Wikimedia\Rdbms\DatabaseDomain::isUnspecified
-        */
-       public function testIsUnspecified() {
-               $domain = new DatabaseDomain( null, null, '' );
-               $this->assertTrue( $domain->isUnspecified() );
-               $domain = new DatabaseDomain( 'mywiki', null, '' );
-               $this->assertFalse( $domain->isUnspecified() );
-               $domain = new DatabaseDomain( 'mywiki', null, '' );
-               $this->assertFalse( $domain->isUnspecified() );
-       }
-}
diff --git a/tests/phpunit/includes/libs/rdbms/database/DatabaseMssqlTest.php b/tests/phpunit/includes/libs/rdbms/database/DatabaseMssqlTest.php
deleted file mode 100644 (file)
index 414042d..0000000
+++ /dev/null
@@ -1,62 +0,0 @@
-<?php
-
-use Wikimedia\Rdbms\Database;
-use Wikimedia\Rdbms\DatabaseMssql;
-
-class DatabaseMssqlTest extends PHPUnit\Framework\TestCase {
-
-       use MediaWikiCoversValidator;
-       use PHPUnit4And6Compat;
-
-       /**
-        * @return PHPUnit_Framework_MockObject_MockObject|DatabaseMssql
-        */
-       private function getMockDb() {
-               return $this->getMockBuilder( DatabaseMssql::class )
-                       ->disableOriginalConstructor()
-                       ->setMethods( null )
-                       ->getMock();
-       }
-
-       public function provideBuildSubstring() {
-               yield [ 'someField', 1, 2, 'SUBSTRING(someField,1,2)' ];
-               yield [ 'someField', 1, null, 'SUBSTRING(someField,1,2147483647)' ];
-               yield [ 'someField', 1, 3333333333, 'SUBSTRING(someField,1,3333333333)' ];
-       }
-
-       /**
-        * @covers Wikimedia\Rdbms\DatabaseMssql::buildSubstring
-        * @dataProvider provideBuildSubstring
-        */
-       public function testBuildSubstring( $input, $start, $length, $expected ) {
-               $mockDb = $this->getMockDb();
-               $output = $mockDb->buildSubstring( $input, $start, $length );
-               $this->assertSame( $expected, $output );
-       }
-
-       public function provideBuildSubstring_invalidParams() {
-               yield [ -1, 1 ];
-               yield [ 1, -1 ];
-               yield [ 1, 'foo' ];
-               yield [ 'foo', 1 ];
-               yield [ null, 1 ];
-               yield [ 0, 1 ];
-       }
-
-       /**
-        * @covers Wikimedia\Rdbms\DatabaseMssql::buildSubstring
-        * @dataProvider provideBuildSubstring_invalidParams
-        */
-       public function testBuildSubstring_invalidParams( $start, $length ) {
-               $mockDb = $this->getMockDb();
-               $this->setExpectedException( InvalidArgumentException::class );
-               $mockDb->buildSubstring( 'foo', $start, $length );
-       }
-
-       /**
-        * @covers \Wikimedia\Rdbms\DatabaseMssql::getAttributes
-        */
-       public function testAttributes() {
-               $this->assertTrue( DatabaseMssql::getAttributes()[Database::ATTR_SCHEMAS_AS_TABLE_GROUPS] );
-       }
-}
diff --git a/tests/phpunit/includes/libs/rdbms/database/DatabaseMysqlBaseTest.php b/tests/phpunit/includes/libs/rdbms/database/DatabaseMysqlBaseTest.php
deleted file mode 100644 (file)
index 4c92545..0000000
+++ /dev/null
@@ -1,740 +0,0 @@
-<?php
-/**
- * Holds tests for DatabaseMysqlBase class.
- *
- * 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; either version 2 of the License, or
- * (at your option) any later version.
- *
- * 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.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- * @author Antoine Musso
- * @copyright © 2013 Antoine Musso
- * @copyright © 2013 Wikimedia Foundation and contributors
- */
-
-use Wikimedia\Rdbms\MySQLMasterPos;
-use Wikimedia\TestingAccessWrapper;
-
-class DatabaseMysqlBaseTest extends PHPUnit\Framework\TestCase {
-
-       use MediaWikiCoversValidator;
-       use PHPUnit4And6Compat;
-
-       /**
-        * @dataProvider provideDiapers
-        * @covers Wikimedia\Rdbms\DatabaseMysqlBase::addIdentifierQuotes
-        */
-       public function testAddIdentifierQuotes( $expected, $in ) {
-               $db = $this->getMockBuilder( DatabaseMysqli::class )
-                       ->disableOriginalConstructor()
-                       ->setMethods( null )
-                       ->getMock();
-
-               $quoted = $db->addIdentifierQuotes( $in );
-               $this->assertEquals( $expected, $quoted );
-       }
-
-       /**
-        * Feeds testAddIdentifierQuotes
-        *
-        * Named per T22281 convention.
-        */
-       public static function provideDiapers() {
-               return [
-                       // Format: expected, input
-                       [ '``', '' ],
-
-                       // Yeah I really hate loosely typed PHP idiocies nowadays
-                       [ '``', null ],
-
-                       // Dear codereviewer, guess what addIdentifierQuotes()
-                       // will return with thoses:
-                       [ '``', false ],
-                       [ '`1`', true ],
-
-                       // We never know what could happen
-                       [ '`0`', 0 ],
-                       [ '`1`', 1 ],
-
-                       // Whatchout! Should probably use something more meaningful
-                       [ "`'`", "'" ],  # single quote
-                       [ '`"`', '"' ],  # double quote
-                       [ '````', '`' ], # backtick
-                       [ '`’`', '’' ],  # apostrophe (look at your encyclopedia)
-
-                       // sneaky NUL bytes are lurking everywhere
-                       [ '``', "\0" ],
-                       [ '`xyzzy`', "\0x\0y\0z\0z\0y\0" ],
-
-                       // unicode chars
-                       [
-                               "`\u{0001}a\u{FFFF}b`",
-                               "\u{0001}a\u{FFFF}b"
-                       ],
-                       [
-                               "`\u{0001}\u{FFFF}`",
-                               "\u{0001}\u{0000}\u{FFFF}\u{0000}"
-                       ],
-                       [ '`☃`', '☃' ],
-                       [ '`メインページ`', 'メインページ' ],
-                       [ '`Басты_бет`', 'Басты_бет' ],
-
-                       // Real world:
-                       [ '`Alix`', 'Alix' ],  # while( ! $recovered ) { sleep(); }
-                       [ '`Backtick: ```', 'Backtick: `' ],
-                       [ '`This is a test`', 'This is a test' ],
-               ];
-       }
-
-       private function getMockForViews() {
-               $db = $this->getMockBuilder( DatabaseMysqli::class )
-                       ->disableOriginalConstructor()
-                       ->setMethods( [ 'fetchRow', 'query', 'getDBname' ] )
-                       ->getMock();
-
-               $db->method( 'query' )
-                       ->with( $this->anything() )
-                       ->willReturn( new FakeResultWrapper( [
-                               (object)[ 'Tables_in_' => 'view1' ],
-                               (object)[ 'Tables_in_' => 'view2' ],
-                               (object)[ 'Tables_in_' => 'myview' ]
-                       ] ) );
-               $db->method( 'getDBname' )->willReturn( '' );
-
-               return $db;
-       }
-
-       /**
-        * @covers Wikimedia\Rdbms\DatabaseMysqlBase::listViews
-        */
-       public function testListviews() {
-               $db = $this->getMockForViews();
-
-               $this->assertEquals( [ 'view1', 'view2', 'myview' ],
-                       $db->listViews() );
-
-               // Prefix filtering
-               $this->assertEquals( [ 'view1', 'view2' ],
-                       $db->listViews( 'view' ) );
-               $this->assertEquals( [ 'myview' ],
-                       $db->listViews( 'my' ) );
-               $this->assertEquals( [],
-                       $db->listViews( 'UNUSED_PREFIX' ) );
-               $this->assertEquals( [ 'view1', 'view2', 'myview' ],
-                       $db->listViews( '' ) );
-       }
-
-       /**
-        * @covers Wikimedia\Rdbms\MySQLMasterPos
-        */
-       public function testBinLogName() {
-               $pos = new MySQLMasterPos( "db1052.2424/4643", 1 );
-
-               $this->assertEquals( "db1052", $pos->getLogName() );
-               $this->assertEquals( "db1052.2424", $pos->getLogFile() );
-               $this->assertEquals( [ 2424, 4643 ], $pos->getLogPosition() );
-       }
-
-       /**
-        * @dataProvider provideComparePositions
-        * @covers Wikimedia\Rdbms\MySQLMasterPos
-        */
-       public function testHasReached(
-               MySQLMasterPos $lowerPos, MySQLMasterPos $higherPos, $match, $hetero
-       ) {
-               if ( $match ) {
-                       $this->assertTrue( $lowerPos->channelsMatch( $higherPos ) );
-
-                       if ( $hetero ) {
-                               // Each position is has one channel higher than the other
-                               $this->assertFalse( $higherPos->hasReached( $lowerPos ) );
-                       } else {
-                               $this->assertTrue( $higherPos->hasReached( $lowerPos ) );
-                       }
-                       $this->assertTrue( $lowerPos->hasReached( $lowerPos ) );
-                       $this->assertTrue( $higherPos->hasReached( $higherPos ) );
-                       $this->assertFalse( $lowerPos->hasReached( $higherPos ) );
-               } else { // channels don't match
-                       $this->assertFalse( $lowerPos->channelsMatch( $higherPos ) );
-
-                       $this->assertFalse( $higherPos->hasReached( $lowerPos ) );
-                       $this->assertFalse( $lowerPos->hasReached( $higherPos ) );
-               }
-       }
-
-       public static function provideComparePositions() {
-               $now = microtime( true );
-
-               return [
-                       // Binlog style
-                       [
-                               new MySQLMasterPos( 'db1034-bin.000976/843431247', $now ),
-                               new MySQLMasterPos( 'db1034-bin.000976/843431248', $now ),
-                               true,
-                               false
-                       ],
-                       [
-                               new MySQLMasterPos( 'db1034-bin.000976/999', $now ),
-                               new MySQLMasterPos( 'db1034-bin.000976/1000', $now ),
-                               true,
-                               false
-                       ],
-                       [
-                               new MySQLMasterPos( 'db1034-bin.000976/999', $now ),
-                               new MySQLMasterPos( 'db1035-bin.000976/1000', $now ),
-                               false,
-                               false
-                       ],
-                       // MySQL GTID style
-                       [
-                               new MySQLMasterPos( '3E11FA47-71CA-11E1-9E33-C80AA9429562:1-23', $now ),
-                               new MySQLMasterPos( '3E11FA47-71CA-11E1-9E33-C80AA9429562:5-24', $now ),
-                               true,
-                               false
-                       ],
-                       [
-                               new MySQLMasterPos( '3E11FA47-71CA-11E1-9E33-C80AA9429562:5-99', $now ),
-                               new MySQLMasterPos( '3E11FA47-71CA-11E1-9E33-C80AA9429562:1-100', $now ),
-                               true,
-                               false
-                       ],
-                       [
-                               new MySQLMasterPos( '3E11FA47-71CA-11E1-9E33-C80AA9429562:1-99', $now ),
-                               new MySQLMasterPos( '1E11FA47-71CA-11E1-9E33-C80AA9429562:1-100', $now ),
-                               false,
-                               false
-                       ],
-                       // MariaDB GTID style
-                       [
-                               new MySQLMasterPos( '255-11-23', $now ),
-                               new MySQLMasterPos( '255-11-24', $now ),
-                               true,
-                               false
-                       ],
-                       [
-                               new MySQLMasterPos( '255-11-99', $now ),
-                               new MySQLMasterPos( '255-11-100', $now ),
-                               true,
-                               false
-                       ],
-                       [
-                               new MySQLMasterPos( '255-11-999', $now ),
-                               new MySQLMasterPos( '254-11-1000', $now ),
-                               false,
-                               false
-                       ],
-                       [
-                               new MySQLMasterPos( '255-11-23,256-12-50', $now ),
-                               new MySQLMasterPos( '255-11-24', $now ),
-                               true,
-                               false
-                       ],
-                       [
-                               new MySQLMasterPos( '255-11-99,256-12-50,257-12-50', $now ),
-                               new MySQLMasterPos( '255-11-1000', $now ),
-                               true,
-                               false
-                       ],
-                       [
-                               new MySQLMasterPos( '255-11-23,256-12-50', $now ),
-                               new MySQLMasterPos( '255-11-24,155-52-63', $now ),
-                               true,
-                               false
-                       ],
-                       [
-                               new MySQLMasterPos( '255-11-99,256-12-50,257-12-50', $now ),
-                               new MySQLMasterPos( '255-11-1000,256-12-51', $now ),
-                               true,
-                               false
-                       ],
-                       [
-                               new MySQLMasterPos( '255-11-99,256-12-50', $now ),
-                               new MySQLMasterPos( '255-13-1000,256-14-49', $now ),
-                               true,
-                               true
-                       ],
-                       [
-                               new MySQLMasterPos( '253-11-999,255-11-999', $now ),
-                               new MySQLMasterPos( '254-11-1000', $now ),
-                               false,
-                               false
-                       ],
-               ];
-       }
-
-       /**
-        * @dataProvider provideChannelPositions
-        * @covers Wikimedia\Rdbms\MySQLMasterPos
-        */
-       public function testChannelsMatch( MySQLMasterPos $pos1, MySQLMasterPos $pos2, $matches ) {
-               $this->assertEquals( $matches, $pos1->channelsMatch( $pos2 ) );
-               $this->assertEquals( $matches, $pos2->channelsMatch( $pos1 ) );
-
-               $roundtripPos = new MySQLMasterPos( (string)$pos1, 1 );
-               $this->assertEquals( (string)$pos1, (string)$roundtripPos );
-       }
-
-       public static function provideChannelPositions() {
-               $now = microtime( true );
-
-               return [
-                       [
-                               new MySQLMasterPos( 'db1034-bin.000876/44', $now ),
-                               new MySQLMasterPos( 'db1034-bin.000976/74', $now ),
-                               true
-                       ],
-                       [
-                               new MySQLMasterPos( 'db1052-bin.000976/999', $now ),
-                               new MySQLMasterPos( 'db1052-bin.000976/1000', $now ),
-                               true
-                       ],
-                       [
-                               new MySQLMasterPos( 'db1066-bin.000976/9999', $now ),
-                               new MySQLMasterPos( 'db1035-bin.000976/10000', $now ),
-                               false
-                       ],
-                       [
-                               new MySQLMasterPos( 'db1066-bin.000976/9999', $now ),
-                               new MySQLMasterPos( 'trump2016.000976/10000', $now ),
-                               false
-                       ],
-               ];
-       }
-
-       /**
-        * @dataProvider provideCommonDomainGTIDs
-        * @covers Wikimedia\Rdbms\MySQLMasterPos
-        */
-       public function testCommonGtidDomains( MySQLMasterPos $pos, MySQLMasterPos $ref, $gtids ) {
-               $this->assertEquals( $gtids, MySQLMasterPos::getCommonDomainGTIDs( $pos, $ref ) );
-       }
-
-       public static function provideCommonDomainGTIDs() {
-               return [
-                       [
-                               new MySQLMasterPos( '255-13-99,256-12-50,257-14-50', 1 ),
-                               new MySQLMasterPos( '255-11-1000', 1 ),
-                               [ '255-13-99' ]
-                       ],
-                       [
-                               new MySQLMasterPos(
-                                       '2E11FA47-71CA-11E1-9E33-C80AA9429562:1-5,' .
-                                       '3E11FA47-71CA-11E1-9E33-C80AA9429562:20-99,' .
-                                       '7E11FA47-71CA-11E1-9E33-C80AA9429562:1-30',
-                                       1
-                               ),
-                               new MySQLMasterPos(
-                                       '1E11FA47-71CA-11E1-9E33-C80AA9429562:30-100,' .
-                                       '3E11FA47-71CA-11E1-9E33-C80AA9429562:30-66',
-                                       1
-                               ),
-                               [ '3E11FA47-71CA-11E1-9E33-C80AA9429562:20-99' ]
-                       ]
-               ];
-       }
-
-       /**
-        * @dataProvider provideLagAmounts
-        * @covers Wikimedia\Rdbms\DatabaseMysqlBase::getLag
-        * @covers Wikimedia\Rdbms\DatabaseMysqlBase::getLagFromPtHeartbeat
-        */
-       public function testPtHeartbeat( $lag ) {
-               $db = $this->getMockBuilder( DatabaseMysqli::class )
-                       ->disableOriginalConstructor()
-                       ->setMethods( [
-                               'getLagDetectionMethod', 'getHeartbeatData', 'getMasterServerInfo' ] )
-                       ->getMock();
-
-               $db->method( 'getLagDetectionMethod' )
-                       ->willReturn( 'pt-heartbeat' );
-
-               $db->method( 'getMasterServerInfo' )
-                       ->willReturn( [ 'serverId' => 172, 'asOf' => time() ] );
-
-               // Fake the current time.
-               list( $nowSecFrac, $nowSec ) = explode( ' ', microtime() );
-               $now = (float)$nowSec + (float)$nowSecFrac;
-               // Fake the heartbeat time.
-               // Work arounds for weak DataTime microseconds support.
-               $ptTime = $now - $lag;
-               $ptSec = (int)$ptTime;
-               $ptSecFrac = ( $ptTime - $ptSec );
-               $ptDateTime = new DateTime( "@$ptSec" );
-               $ptTimeISO = $ptDateTime->format( 'Y-m-d\TH:i:s' );
-               $ptTimeISO .= ltrim( number_format( $ptSecFrac, 6 ), '0' );
-
-               $db->method( 'getHeartbeatData' )
-                       ->with( [ 'server_id' => 172 ] )
-                       ->willReturn( [ $ptTimeISO, $now ] );
-
-               $db->setLBInfo( 'clusterMasterHost', 'db1052' );
-               $lagEst = $db->getLag();
-
-               $this->assertGreaterThan( $lag - 0.010, $lagEst, "Correct heatbeat lag" );
-               $this->assertLessThan( $lag + 0.010, $lagEst, "Correct heatbeat lag" );
-       }
-
-       public static function provideLagAmounts() {
-               return [
-                       [ 0 ],
-                       [ 0.3 ],
-                       [ 6.5 ],
-                       [ 10.1 ],
-                       [ 200.2 ],
-                       [ 400.7 ],
-                       [ 600.22 ],
-                       [ 1000.77 ],
-               ];
-       }
-
-       /**
-        * @dataProvider provideGtidData
-        * @covers Wikimedia\Rdbms\MySQLMasterPos
-        * @covers Wikimedia\Rdbms\DatabaseMysqlBase::getReplicaPos
-        * @covers Wikimedia\Rdbms\DatabaseMysqlBase::getMasterPos
-        */
-       public function testServerGtidTable( $gtable, $rBLtable, $mBLtable, $rGTIDs, $mGTIDs ) {
-               $db = $this->getMockBuilder( DatabaseMysqli::class )
-                       ->disableOriginalConstructor()
-                       ->setMethods( [
-                               'useGTIDs',
-                               'getServerGTIDs',
-                               'getServerRoleStatus',
-                               'getServerId',
-                               'getServerUUID'
-                       ] )
-                       ->getMock();
-
-               $db->method( 'useGTIDs' )->willReturn( true );
-               $db->method( 'getServerGTIDs' )->willReturn( $gtable );
-               $db->method( 'getServerRoleStatus' )->willReturnCallback(
-                       function ( $role ) use ( $rBLtable, $mBLtable ) {
-                               if ( $role === 'SLAVE' ) {
-                                       return $rBLtable;
-                               } elseif ( $role === 'MASTER' ) {
-                                       return $mBLtable;
-                               }
-
-                               return null;
-                       }
-               );
-               $db->method( 'getServerId' )->willReturn( 1 );
-               $db->method( 'getServerUUID' )->willReturn( '2E11FA47-71CA-11E1-9E33-C80AA9429562' );
-
-               if ( is_array( $rGTIDs ) ) {
-                       $this->assertEquals( $rGTIDs, $db->getReplicaPos()->getGTIDs() );
-               } else {
-                       $this->assertEquals( false, $db->getReplicaPos() );
-               }
-               if ( is_array( $mGTIDs ) ) {
-                       $this->assertEquals( $mGTIDs, $db->getMasterPos()->getGTIDs() );
-               } else {
-                       $this->assertEquals( false, $db->getMasterPos() );
-               }
-       }
-
-       public static function provideGtidData() {
-               return [
-                       // MariaDB
-                       [
-                               [
-                                       'gtid_domain_id' => 100,
-                                       'gtid_current_pos' => '100-13-77',
-                                       'gtid_binlog_pos' => '100-13-77',
-                                       'gtid_slave_pos' => null // master
-                               ],
-                               [
-                                       'Relay_Master_Log_File' => 'host.1600',
-                                       'Exec_Master_Log_Pos' => '77'
-                               ],
-                               [
-                                       'File' => 'host.1600',
-                                       'Position' => '77'
-                               ],
-                               [],
-                               [ '100' => '100-13-77' ]
-                       ],
-                       [
-                               [
-                                       'gtid_domain_id' => 100,
-                                       'gtid_current_pos' => '100-13-77',
-                                       'gtid_binlog_pos' => '100-13-77',
-                                       'gtid_slave_pos' => '100-13-77' // replica
-                               ],
-                               [
-                                       'Relay_Master_Log_File' => 'host.1600',
-                                       'Exec_Master_Log_Pos' => '77'
-                               ],
-                               [],
-                               [ '100' => '100-13-77' ],
-                               [ '100' => '100-13-77' ]
-                       ],
-                       [
-                               [
-                                       'gtid_current_pos' => '100-13-77',
-                                       'gtid_binlog_pos' => '100-13-77',
-                                       'gtid_slave_pos' => '100-13-77' // replica
-                               ],
-                               [
-                                       'Relay_Master_Log_File' => 'host.1600',
-                                       'Exec_Master_Log_Pos' => '77'
-                               ],
-                               [],
-                               [ '100' => '100-13-77' ],
-                               [ '100' => '100-13-77' ]
-                       ],
-                       // MySQL
-                       [
-                               [
-                                       'gtid_executed' => '2E11FA47-71CA-11E1-9E33-C80AA9429562:1-77'
-                               ],
-                               [
-                                       'Relay_Master_Log_File' => 'host.1600',
-                                       'Exec_Master_Log_Pos' => '77'
-                               ],
-                               [], // only a replica
-                               [ '2E11FA47-71CA-11E1-9E33-C80AA9429562'
-                                       => '2E11FA47-71CA-11E1-9E33-C80AA9429562:1-77' ],
-                               // replica/master use same var
-                               [ '2E11FA47-71CA-11E1-9E33-C80AA9429562'
-                                       => '2E11FA47-71CA-11E1-9E33-C80AA9429562:1-77' ],
-                       ],
-                       [
-                               [
-                                       'gtid_executed' => '2E11FA47-71CA-11E1-9E33-C80AA9429562:1-49,' .
-                                               '2E11FA47-71CA-11E1-9E33-C80AA9429562:51-77'
-                               ],
-                               [
-                                       'Relay_Master_Log_File' => 'host.1600',
-                                       'Exec_Master_Log_Pos' => '77'
-                               ],
-                               [], // only a replica
-                               [ '2E11FA47-71CA-11E1-9E33-C80AA9429562'
-                                       => '2E11FA47-71CA-11E1-9E33-C80AA9429562:51-77' ],
-                               // replica/master use same var
-                               [ '2E11FA47-71CA-11E1-9E33-C80AA9429562'
-                                       => '2E11FA47-71CA-11E1-9E33-C80AA9429562:51-77' ],
-                       ],
-                       [
-                               [
-                                       'gtid_executed' => null, // not enabled?
-                                       'gtid_binlog_pos' => null
-                               ],
-                               [
-                                       'Relay_Master_Log_File' => 'host.1600',
-                                       'Exec_Master_Log_Pos' => '77'
-                               ],
-                               [], // only a replica
-                               [], // binlog fallback
-                               false
-                       ],
-                       [
-                               [
-                                       'gtid_executed' => null, // not enabled?
-                                       'gtid_binlog_pos' => null
-                               ],
-                               [], // no replication
-                               [], // no replication
-                               false,
-                               false
-                       ]
-               ];
-       }
-
-       /**
-        * @covers Wikimedia\Rdbms\MySQLMasterPos
-        */
-       public function testSerialize() {
-               $pos = new MySQLMasterPos( '3E11FA47-71CA-11E1-9E33-C80AA9429562:99', 53636363 );
-               $roundtripPos = unserialize( serialize( $pos ) );
-
-               $this->assertEquals( $pos, $roundtripPos );
-
-               $pos = new MySQLMasterPos( '255-11-23', 53636363 );
-               $roundtripPos = unserialize( serialize( $pos ) );
-
-               $this->assertEquals( $pos, $roundtripPos );
-       }
-
-       /**
-        * @covers Wikimedia\Rdbms\DatabaseMysqlBase::isInsertSelectSafe
-        * @dataProvider provideInsertSelectCases
-        */
-       public function testInsertSelectIsSafe( $insertOpts, $selectOpts, $row, $safe ) {
-               $db = $this->getMockBuilder( DatabaseMysqli::class )
-                       ->disableOriginalConstructor()
-                       ->setMethods( [ 'getReplicationSafetyInfo' ] )
-                       ->getMock();
-               $db->method( 'getReplicationSafetyInfo' )->willReturn( (object)$row );
-               $dbw = TestingAccessWrapper::newFromObject( $db );
-
-               $this->assertEquals( $safe, $dbw->isInsertSelectSafe( $insertOpts, $selectOpts ) );
-       }
-
-       public function provideInsertSelectCases() {
-               return [
-                       [
-                               [],
-                               [],
-                               [
-                                       'innodb_autoinc_lock_mode' => '2',
-                                       'binlog_format' => 'ROW',
-                               ],
-                               true
-                       ],
-                       [
-                               [],
-                               [ 'LIMIT' => 100 ],
-                               [
-                                       'innodb_autoinc_lock_mode' => '2',
-                                       'binlog_format' => 'ROW',
-                               ],
-                               true
-                       ],
-                       [
-                               [],
-                               [ 'LIMIT' => 100 ],
-                               [
-                                       'innodb_autoinc_lock_mode' => '0',
-                                       'binlog_format' => 'STATEMENT',
-                               ],
-                               false
-                       ],
-                       [
-                               [],
-                               [],
-                               [
-                                       'innodb_autoinc_lock_mode' => '2',
-                                       'binlog_format' => 'STATEMENT',
-                               ],
-                               false
-                       ],
-                       [
-                               [ 'NO_AUTO_COLUMNS' ],
-                               [ 'LIMIT' => 100 ],
-                               [
-                                       'innodb_autoinc_lock_mode' => '0',
-                                       'binlog_format' => 'STATEMENT',
-                               ],
-                               false
-                       ],
-                       [
-                               [],
-                               [],
-                               [
-                                       'innodb_autoinc_lock_mode' => 0,
-                                       'binlog_format' => 'STATEMENT',
-                               ],
-                               true
-                       ],
-                       [
-                               [ 'NO_AUTO_COLUMNS' ],
-                               [],
-                               [
-                                       'innodb_autoinc_lock_mode' => 2,
-                                       'binlog_format' => 'STATEMENT',
-                               ],
-                               true
-                       ],
-                       [
-                               [ 'NO_AUTO_COLUMNS' ],
-                               [],
-                               [
-                                       'innodb_autoinc_lock_mode' => 0,
-                                       'binlog_format' => 'STATEMENT',
-                               ],
-                               true
-                       ],
-
-               ];
-       }
-
-       /**
-        * @covers \Wikimedia\Rdbms\DatabaseMysqlBase::buildIntegerCast
-        */
-       public function testBuildIntegerCast() {
-               $db = $this->getMockBuilder( DatabaseMysqli::class )
-                       ->disableOriginalConstructor()
-                       ->setMethods( null )
-                       ->getMock();
-               $output = $db->buildIntegerCast( 'fieldName' );
-               $this->assertSame( 'CAST( fieldName AS SIGNED )', $output );
-       }
-
-       /**
-        * @covers Wikimedia\Rdbms\Database::setIndexAliases
-        */
-       public function testIndexAliases() {
-               $db = $this->getMockBuilder( DatabaseMysqli::class )
-                       ->disableOriginalConstructor()
-                       ->setMethods( [ 'mysqlRealEscapeString', 'dbSchema', 'tablePrefix' ] )
-                       ->getMock();
-               $db->method( 'mysqlRealEscapeString' )->willReturnCallback(
-                       function ( $s ) {
-                               return str_replace( "'", "\\'", $s );
-                       }
-               );
-
-               $db->setIndexAliases( [ 'a_b_idx' => 'a_c_idx' ] );
-               $sql = $db->selectSQLText(
-                       'zend', 'field', [ 'a' => 'x' ], __METHOD__, [ 'USE INDEX' => 'a_b_idx' ] );
-
-               $this->assertEquals(
-                       "SELECT  field  FROM `zend`  FORCE INDEX (a_c_idx)  WHERE a = 'x'  ",
-                       $sql
-               );
-
-               $db->setIndexAliases( [] );
-               $sql = $db->selectSQLText(
-                       'zend', 'field', [ 'a' => 'x' ], __METHOD__, [ 'USE INDEX' => 'a_b_idx' ] );
-
-               $this->assertEquals(
-                       "SELECT  field  FROM `zend`  FORCE INDEX (a_b_idx)  WHERE a = 'x'  ",
-                       $sql
-               );
-       }
-
-       /**
-        * @covers Wikimedia\Rdbms\Database::setTableAliases
-        */
-       public function testTableAliases() {
-               $db = $this->getMockBuilder( DatabaseMysqli::class )
-                       ->disableOriginalConstructor()
-                       ->setMethods( [ 'mysqlRealEscapeString', 'dbSchema', 'tablePrefix' ] )
-                       ->getMock();
-               $db->method( 'mysqlRealEscapeString' )->willReturnCallback(
-                       function ( $s ) {
-                               return str_replace( "'", "\\'", $s );
-                       }
-               );
-
-               $db->setTableAliases( [
-                       'meow' => [ 'dbname' => 'feline', 'schema' => null, 'prefix' => 'cat_' ]
-               ] );
-               $sql = $db->selectSQLText( 'meow', 'field', [ 'a' => 'x' ], __METHOD__ );
-
-               $this->assertEquals(
-                       "SELECT  field  FROM `feline`.`cat_meow`    WHERE a = 'x'  ",
-                       $sql
-               );
-
-               $db->setTableAliases( [] );
-               $sql = $db->selectSQLText( 'meow', 'field', [ 'a' => 'x' ], __METHOD__ );
-
-               $this->assertEquals(
-                       "SELECT  field  FROM `meow`    WHERE a = 'x'  ",
-                       $sql
-               );
-       }
-}
diff --git a/tests/phpunit/includes/libs/rdbms/database/DatabaseSQLTest.php b/tests/phpunit/includes/libs/rdbms/database/DatabaseSQLTest.php
deleted file mode 100644 (file)
index 0e133d8..0000000
+++ /dev/null
@@ -1,2164 +0,0 @@
-<?php
-
-use Wikimedia\Rdbms\IDatabase;
-use Wikimedia\Rdbms\LikeMatch;
-use Wikimedia\Rdbms\Database;
-use Wikimedia\TestingAccessWrapper;
-use Wikimedia\Rdbms\DBTransactionStateError;
-use Wikimedia\Rdbms\DBUnexpectedError;
-use Wikimedia\Rdbms\DBTransactionError;
-
-/**
- * Test the parts of the Database abstract class that deal
- * with creating SQL text.
- */
-class DatabaseSQLTest extends PHPUnit\Framework\TestCase {
-
-       use MediaWikiCoversValidator;
-       use PHPUnit4And6Compat;
-
-       /** @var DatabaseTestHelper|Database */
-       private $database;
-
-       protected function setUp() {
-               parent::setUp();
-               $this->database = new DatabaseTestHelper( __CLASS__, [ 'cliMode' => true ] );
-       }
-
-       protected function assertLastSql( $sqlText ) {
-               $this->assertEquals(
-                       $sqlText,
-                       $this->database->getLastSqls()
-               );
-       }
-
-       protected function assertLastSqlDb( $sqlText, DatabaseTestHelper $db ) {
-               $this->assertEquals( $sqlText, $db->getLastSqls() );
-       }
-
-       /**
-        * @dataProvider provideSelect
-        * @covers Wikimedia\Rdbms\Database::select
-        * @covers Wikimedia\Rdbms\Database::selectSQLText
-        * @covers Wikimedia\Rdbms\Database::tableNamesWithIndexClauseOrJOIN
-        * @covers Wikimedia\Rdbms\Database::useIndexClause
-        * @covers Wikimedia\Rdbms\Database::ignoreIndexClause
-        * @covers Wikimedia\Rdbms\Database::makeSelectOptions
-        * @covers Wikimedia\Rdbms\Database::makeOrderBy
-        * @covers Wikimedia\Rdbms\Database::makeGroupByWithHaving
-        * @covers Wikimedia\Rdbms\Database::selectFieldsOrOptionsAggregate
-        * @covers Wikimedia\Rdbms\Database::selectOptionsIncludeLocking
-        */
-       public function testSelect( $sql, $sqlText ) {
-               $this->database->select(
-                       $sql['tables'],
-                       $sql['fields'],
-                       $sql['conds'] ?? [],
-                       __METHOD__,
-                       $sql['options'] ?? [],
-                       $sql['join_conds'] ?? []
-               );
-               $this->assertLastSql( $sqlText );
-       }
-
-       public static function provideSelect() {
-               return [
-                       [
-                               [
-                                       'tables' => 'table',
-                                       'fields' => [ 'field', 'alias' => 'field2' ],
-                                       'conds' => [ 'alias' => 'text' ],
-                               ],
-                               "SELECT field,field2 AS alias " .
-                                       "FROM table " .
-                                       "WHERE alias = 'text'"
-                       ],
-                       [
-                               [
-                                       'tables' => 'table',
-                                       'fields' => [ 'field', 'alias' => 'field2' ],
-                                       'conds' => 'alias = \'text\'',
-                               ],
-                               "SELECT field,field2 AS alias " .
-                               "FROM table " .
-                               "WHERE alias = 'text'"
-                       ],
-                       [
-                               [
-                                       'tables' => 'table',
-                                       'fields' => [ 'field', 'alias' => 'field2' ],
-                                       'conds' => [],
-                               ],
-                               "SELECT field,field2 AS alias " .
-                               "FROM table"
-                       ],
-                       [
-                               [
-                                       'tables' => 'table',
-                                       'fields' => [ 'field', 'alias' => 'field2' ],
-                                       'conds' => '',
-                               ],
-                               "SELECT field,field2 AS alias " .
-                               "FROM table"
-                       ],
-                       [
-                               [
-                                       'tables' => 'table',
-                                       'fields' => [ 'field', 'alias' => 'field2' ],
-                                       'conds' => '0', // T188314
-                               ],
-                               "SELECT field,field2 AS alias " .
-                               "FROM table " .
-                               "WHERE 0"
-                       ],
-                       [
-                               [
-                                       // 'tables' with space prepended indicates pre-escaped table name
-                                       'tables' => ' table LEFT JOIN table2',
-                                       'fields' => [ 'field' ],
-                                       'conds' => [ 'field' => 'text' ],
-                               ],
-                               "SELECT field FROM  table LEFT JOIN table2 WHERE field = 'text'"
-                       ],
-                       [
-                               [
-                                       // Empty 'tables' is allowed
-                                       'tables' => '',
-                                       'fields' => [ 'SPECIAL_QUERY()' ],
-                               ],
-                               "SELECT SPECIAL_QUERY()"
-                       ],
-                       [
-                               [
-                                       'tables' => 'table',
-                                       'fields' => [ 'field', 'alias' => 'field2' ],
-                                       'conds' => [ 'alias' => 'text' ],
-                                       'options' => [ 'LIMIT' => 1, 'ORDER BY' => 'field' ],
-                               ],
-                               "SELECT field,field2 AS alias " .
-                                       "FROM table " .
-                                       "WHERE alias = 'text' " .
-                                       "ORDER BY field " .
-                                       "LIMIT 1"
-                       ],
-                       [
-                               [
-                                       'tables' => [ 'table', 't2' => 'table2' ],
-                                       'fields' => [ 'tid', 'field', 'alias' => 'field2', 't2.id' ],
-                                       'conds' => [ 'alias' => 'text' ],
-                                       'options' => [ 'LIMIT' => 1, 'ORDER BY' => 'field' ],
-                                       'join_conds' => [ 't2' => [
-                                               'LEFT JOIN', 'tid = t2.id'
-                                       ] ],
-                               ],
-                               "SELECT tid,field,field2 AS alias,t2.id " .
-                                       "FROM table LEFT JOIN table2 t2 ON ((tid = t2.id)) " .
-                                       "WHERE alias = 'text' " .
-                                       "ORDER BY field " .
-                                       "LIMIT 1"
-                       ],
-                       [
-                               [
-                                       'tables' => [ 'table', 't2' => 'table2' ],
-                                       'fields' => [ 'tid', 'field', 'alias' => 'field2', 't2.id' ],
-                                       'conds' => [ 'alias' => 'text' ],
-                                       'options' => [ 'LIMIT' => 1, 'GROUP BY' => 'field', 'HAVING' => 'COUNT(*) > 1' ],
-                                       'join_conds' => [ 't2' => [
-                                               'LEFT JOIN', 'tid = t2.id'
-                                       ] ],
-                               ],
-                               "SELECT tid,field,field2 AS alias,t2.id " .
-                                       "FROM table LEFT JOIN table2 t2 ON ((tid = t2.id)) " .
-                                       "WHERE alias = 'text' " .
-                                       "GROUP BY field HAVING COUNT(*) > 1 " .
-                                       "LIMIT 1"
-                       ],
-                       [
-                               [
-                                       'tables' => [ 'table', 't2' => 'table2' ],
-                                       'fields' => [ 'tid', 'field', 'alias' => 'field2', 't2.id' ],
-                                       'conds' => [ 'alias' => 'text' ],
-                                       'options' => [
-                                               'LIMIT' => 1,
-                                               'GROUP BY' => [ 'field', 'field2' ],
-                                               'HAVING' => [ 'COUNT(*) > 1', 'field' => 1 ]
-                                       ],
-                                       'join_conds' => [ 't2' => [
-                                               'LEFT JOIN', 'tid = t2.id'
-                                       ] ],
-                               ],
-                               "SELECT tid,field,field2 AS alias,t2.id " .
-                                       "FROM table LEFT JOIN table2 t2 ON ((tid = t2.id)) " .
-                                       "WHERE alias = 'text' " .
-                                       "GROUP BY field,field2 HAVING (COUNT(*) > 1) AND field = '1' " .
-                                       "LIMIT 1"
-                       ],
-                       [
-                               [
-                                       'tables' => [ 'table' ],
-                                       'fields' => [ 'alias' => 'field' ],
-                                       'conds' => [ 'alias' => [ 1, 2, 3, 4 ] ],
-                               ],
-                               "SELECT field AS alias " .
-                                       "FROM table " .
-                                       "WHERE alias IN ('1','2','3','4')"
-                       ],
-                       [
-                               [
-                                       'tables' => 'table',
-                                       'fields' => [ 'field' ],
-                                       'options' => [ 'USE INDEX' => [ 'table' => 'X' ] ],
-                               ],
-                               // No-op by default
-                               "SELECT field FROM table"
-                       ],
-                       [
-                               [
-                                       'tables' => 'table',
-                                       'fields' => [ 'field' ],
-                                       'options' => [ 'IGNORE INDEX' => [ 'table' => 'X' ] ],
-                               ],
-                               // No-op by default
-                               "SELECT field FROM table"
-                       ],
-                       [
-                               [
-                                       'tables' => 'table',
-                                       'fields' => [ 'field' ],
-                                       'options' => [ 'DISTINCT' ],
-                               ],
-                               "SELECT DISTINCT field FROM table"
-                       ],
-                       [
-                               [
-                                       'tables' => 'table',
-                                       'fields' => [ 'field' ],
-                                       'options' => [ 'LOCK IN SHARE MODE' ],
-                               ],
-                               "SELECT field FROM table      LOCK IN SHARE MODE"
-                       ],
-                       [
-                               [
-                                       'tables' => 'table',
-                                       'fields' => [ 'field' ],
-                                       'options' => [ 'EXPLAIN' => true ],
-                               ],
-                               'EXPLAIN SELECT field FROM table'
-                       ],
-                       [
-                               [
-                                       'tables' => 'table',
-                                       'fields' => [ 'field' ],
-                                       'options' => [ 'FOR UPDATE' ],
-                               ],
-                               "SELECT field FROM table      FOR UPDATE"
-                       ],
-               ];
-       }
-
-       /**
-        * @dataProvider provideLockForUpdate
-        * @covers Wikimedia\Rdbms\Database::lockForUpdate
-        */
-       public function testLockForUpdate( $sql, $sqlText ) {
-               $this->database->startAtomic( __METHOD__ );
-               $this->database->lockForUpdate(
-                       $sql['tables'],
-                       $sql['conds'] ?? [],
-                       __METHOD__,
-                       $sql['options'] ?? [],
-                       $sql['join_conds'] ?? []
-               );
-               $this->database->endAtomic( __METHOD__ );
-
-               $this->assertLastSql( "BEGIN; $sqlText; COMMIT" );
-       }
-
-       public static function provideLockForUpdate() {
-               return [
-                       [
-                               [
-                                       'tables' => [ 'table' ],
-                                       'conds' => [ 'field' => [ 1, 2, 3, 4 ] ],
-                               ],
-                               "SELECT COUNT(*) AS rowcount FROM " .
-                               "(SELECT 1 FROM table WHERE field IN ('1','2','3','4')    " .
-                               "FOR UPDATE) tmp_count"
-                       ],
-                       [
-                               [
-                                       'tables' => [ 'table', 't2' => 'table2' ],
-                                       'conds' => [ 'field' => 'text' ],
-                                       'options' => [ 'LIMIT' => 1, 'ORDER BY' => 'field' ],
-                                       'join_conds' => [ 't2' => [
-                                               'LEFT JOIN', 'tid = t2.id'
-                                       ] ],
-                               ],
-                               "SELECT COUNT(*) AS rowcount FROM " .
-                               "(SELECT 1 FROM table LEFT JOIN table2 t2 ON ((tid = t2.id)) " .
-                               "WHERE field = 'text' ORDER BY field LIMIT 1   FOR UPDATE) tmp_count"
-                       ],
-                       [
-                               [
-                                       'tables' => 'table',
-                               ],
-                               "SELECT COUNT(*) AS rowcount FROM " .
-                               "(SELECT 1 FROM table      FOR UPDATE) tmp_count"
-                       ],
-               ];
-       }
-
-       /**
-        * @covers Wikimedia\Rdbms\Subquery
-        * @dataProvider provideSelectRowCount
-        * @param array $sql
-        * @param string $sqlText
-        */
-       public function testSelectRowCount( $sql, $sqlText ) {
-               $this->database->selectRowCount(
-                       $sql['tables'],
-                       $sql['field'],
-                       $sql['conds'] ?? [],
-                       __METHOD__,
-                       $sql['options'] ?? [],
-                       $sql['join_conds'] ?? []
-               );
-               $this->assertLastSql( $sqlText );
-       }
-
-       public static function provideSelectRowCount() {
-               return [
-                       [
-                               [
-                                       'tables' => 'table',
-                                       'field' => [ '*' ],
-                                       'conds' => [ 'field' => 'text' ],
-                               ],
-                               "SELECT COUNT(*) AS rowcount FROM " .
-                               "(SELECT 1 FROM table WHERE field = 'text'  ) tmp_count"
-                       ],
-                       [
-                               [
-                                       'tables' => 'table',
-                                       'field' => [ 'column' ],
-                                       'conds' => [ 'field' => 'text' ],
-                               ],
-                               "SELECT COUNT(*) AS rowcount FROM " .
-                               "(SELECT 1 FROM table WHERE field = 'text' AND (column IS NOT NULL)  ) tmp_count"
-                       ],
-                       [
-                               [
-                                       'tables' => 'table',
-                                       'field' => [ 'alias' => 'column' ],
-                                       'conds' => [ 'field' => 'text' ],
-                               ],
-                               "SELECT COUNT(*) AS rowcount FROM " .
-                               "(SELECT 1 FROM table WHERE field = 'text' AND (column IS NOT NULL)  ) tmp_count"
-                       ],
-                       [
-                               [
-                                       'tables' => 'table',
-                                       'field' => [ 'alias' => 'column' ],
-                                       'conds' => '',
-                               ],
-                               "SELECT COUNT(*) AS rowcount FROM " .
-                               "(SELECT 1 FROM table WHERE (column IS NOT NULL)  ) tmp_count"
-                       ],
-                       [
-                               [
-                                       'tables' => 'table',
-                                       'field' => [ 'alias' => 'column' ],
-                                       'conds' => false,
-                               ],
-                               "SELECT COUNT(*) AS rowcount FROM " .
-                               "(SELECT 1 FROM table WHERE (column IS NOT NULL)  ) tmp_count"
-                       ],
-                       [
-                               [
-                                       'tables' => 'table',
-                                       'field' => [ 'alias' => 'column' ],
-                                       'conds' => null,
-                               ],
-                               "SELECT COUNT(*) AS rowcount FROM " .
-                               "(SELECT 1 FROM table WHERE (column IS NOT NULL)  ) tmp_count"
-                       ],
-                       [
-                               [
-                                       'tables' => 'table',
-                                       'field' => [ 'alias' => 'column' ],
-                                       'conds' => '1',
-                               ],
-                               "SELECT COUNT(*) AS rowcount FROM " .
-                               "(SELECT 1 FROM table WHERE (1) AND (column IS NOT NULL)  ) tmp_count"
-                       ],
-                       [
-                               [
-                                       'tables' => 'table',
-                                       'field' => [ 'alias' => 'column' ],
-                                       'conds' => '0',
-                               ],
-                               "SELECT COUNT(*) AS rowcount FROM " .
-                               "(SELECT 1 FROM table WHERE (0) AND (column IS NOT NULL)  ) tmp_count"
-                       ],
-               ];
-       }
-
-       /**
-        * @dataProvider provideUpdate
-        * @covers Wikimedia\Rdbms\Database::update
-        * @covers Wikimedia\Rdbms\Database::makeUpdateOptions
-        * @covers Wikimedia\Rdbms\Database::makeUpdateOptionsArray
-        */
-       public function testUpdate( $sql, $sqlText ) {
-               $this->database->update(
-                       $sql['table'],
-                       $sql['values'],
-                       $sql['conds'],
-                       __METHOD__,
-                       $sql['options'] ?? []
-               );
-               $this->assertLastSql( $sqlText );
-       }
-
-       public static function provideUpdate() {
-               return [
-                       [
-                               [
-                                       'table' => 'table',
-                                       'values' => [ 'field' => 'text', 'field2' => 'text2' ],
-                                       'conds' => [ 'alias' => 'text' ],
-                               ],
-                               "UPDATE table " .
-                                       "SET field = 'text'" .
-                                       ",field2 = 'text2' " .
-                                       "WHERE alias = 'text'"
-                       ],
-                       [
-                               [
-                                       'table' => 'table',
-                                       'values' => [ 'field = other', 'field2' => 'text2' ],
-                                       'conds' => [ 'id' => '1' ],
-                               ],
-                               "UPDATE table " .
-                                       "SET field = other" .
-                                       ",field2 = 'text2' " .
-                                       "WHERE id = '1'"
-                       ],
-                       [
-                               [
-                                       'table' => 'table',
-                                       'values' => [ 'field = other', 'field2' => 'text2' ],
-                                       'conds' => '*',
-                               ],
-                               "UPDATE table " .
-                                       "SET field = other" .
-                                       ",field2 = 'text2'"
-                       ],
-               ];
-       }
-
-       /**
-        * @dataProvider provideDelete
-        * @covers Wikimedia\Rdbms\Database::delete
-        */
-       public function testDelete( $sql, $sqlText ) {
-               $this->database->delete(
-                       $sql['table'],
-                       $sql['conds'],
-                       __METHOD__
-               );
-               $this->assertLastSql( $sqlText );
-       }
-
-       public static function provideDelete() {
-               return [
-                       [
-                               [
-                                       'table' => 'table',
-                                       'conds' => [ 'alias' => 'text' ],
-                               ],
-                               "DELETE FROM table " .
-                                       "WHERE alias = 'text'"
-                       ],
-                       [
-                               [
-                                       'table' => 'table',
-                                       'conds' => '*',
-                               ],
-                               "DELETE FROM table"
-                       ],
-               ];
-       }
-
-       /**
-        * @dataProvider provideUpsert
-        * @covers Wikimedia\Rdbms\Database::upsert
-        */
-       public function testUpsert( $sql, $sqlText ) {
-               $this->database->upsert(
-                       $sql['table'],
-                       $sql['rows'],
-                       $sql['uniqueIndexes'],
-                       $sql['set'],
-                       __METHOD__
-               );
-               $this->assertLastSql( $sqlText );
-       }
-
-       public static function provideUpsert() {
-               return [
-                       [
-                               [
-                                       'table' => 'upsert_table',
-                                       'rows' => [ 'field' => 'text', 'field2' => 'text2' ],
-                                       'uniqueIndexes' => [ 'field' ],
-                                       'set' => [ 'field' => 'set' ],
-                               ],
-                               "BEGIN; " .
-                                       "UPDATE upsert_table " .
-                                       "SET field = 'set' " .
-                                       "WHERE ((field = 'text')); " .
-                                       "INSERT IGNORE INTO upsert_table " .
-                                       "(field,field2) " .
-                                       "VALUES ('text','text2'); " .
-                                       "COMMIT"
-                       ],
-               ];
-       }
-
-       /**
-        * @dataProvider provideDeleteJoin
-        * @covers Wikimedia\Rdbms\Database::deleteJoin
-        */
-       public function testDeleteJoin( $sql, $sqlText ) {
-               $this->database->deleteJoin(
-                       $sql['delTable'],
-                       $sql['joinTable'],
-                       $sql['delVar'],
-                       $sql['joinVar'],
-                       $sql['conds'],
-                       __METHOD__
-               );
-               $this->assertLastSql( $sqlText );
-       }
-
-       public static function provideDeleteJoin() {
-               return [
-                       [
-                               [
-                                       'delTable' => 'table',
-                                       'joinTable' => 'table_join',
-                                       'delVar' => 'field',
-                                       'joinVar' => 'field_join',
-                                       'conds' => [ 'alias' => 'text' ],
-                               ],
-                               "DELETE FROM table " .
-                                       "WHERE field IN (" .
-                                       "SELECT field_join FROM table_join WHERE alias = 'text'" .
-                                       ")"
-                       ],
-                       [
-                               [
-                                       'delTable' => 'table',
-                                       'joinTable' => 'table_join',
-                                       'delVar' => 'field',
-                                       'joinVar' => 'field_join',
-                                       'conds' => '*',
-                               ],
-                               "DELETE FROM table " .
-                                       "WHERE field IN (" .
-                                       "SELECT field_join FROM table_join " .
-                                       ")"
-                       ],
-               ];
-       }
-
-       /**
-        * @dataProvider provideInsert
-        * @covers Wikimedia\Rdbms\Database::insert
-        * @covers Wikimedia\Rdbms\Database::makeInsertOptions
-        */
-       public function testInsert( $sql, $sqlText ) {
-               $this->database->insert(
-                       $sql['table'],
-                       $sql['rows'],
-                       __METHOD__,
-                       $sql['options'] ?? []
-               );
-               $this->assertLastSql( $sqlText );
-       }
-
-       public static function provideInsert() {
-               return [
-                       [
-                               [
-                                       'table' => 'table',
-                                       'rows' => [ 'field' => 'text', 'field2' => 2 ],
-                               ],
-                               "INSERT INTO table " .
-                                       "(field,field2) " .
-                                       "VALUES ('text','2')"
-                       ],
-                       [
-                               [
-                                       'table' => 'table',
-                                       'rows' => [ 'field' => 'text', 'field2' => 2 ],
-                                       'options' => 'IGNORE',
-                               ],
-                               "INSERT IGNORE INTO table " .
-                                       "(field,field2) " .
-                                       "VALUES ('text','2')"
-                       ],
-                       [
-                               [
-                                       'table' => 'table',
-                                       'rows' => [
-                                               [ 'field' => 'text', 'field2' => 2 ],
-                                               [ 'field' => 'multi', 'field2' => 3 ],
-                                       ],
-                                       'options' => 'IGNORE',
-                               ],
-                               "INSERT IGNORE INTO table " .
-                                       "(field,field2) " .
-                                       "VALUES " .
-                                       "('text','2')," .
-                                       "('multi','3')"
-                       ],
-               ];
-       }
-
-       /**
-        * @dataProvider provideInsertSelect
-        * @covers Wikimedia\Rdbms\Database::insertSelect
-        * @covers Wikimedia\Rdbms\Database::nativeInsertSelect
-        */
-       public function testInsertSelect( $sql, $sqlTextNative, $sqlSelect, $sqlInsert ) {
-               $this->database->insertSelect(
-                       $sql['destTable'],
-                       $sql['srcTable'],
-                       $sql['varMap'],
-                       $sql['conds'],
-                       __METHOD__,
-                       $sql['insertOptions'] ?? [],
-                       $sql['selectOptions'] ?? [],
-                       $sql['selectJoinConds'] ?? []
-               );
-               $this->assertLastSql( $sqlTextNative );
-
-               $dbWeb = new DatabaseTestHelper( __CLASS__, [ 'cliMode' => false ] );
-               $dbWeb->forceNextResult( [
-                       array_flip( array_keys( $sql['varMap'] ) )
-               ] );
-               $dbWeb->insertSelect(
-                       $sql['destTable'],
-                       $sql['srcTable'],
-                       $sql['varMap'],
-                       $sql['conds'],
-                       __METHOD__,
-                       $sql['insertOptions'] ?? [],
-                       $sql['selectOptions'] ?? [],
-                       $sql['selectJoinConds'] ?? []
-               );
-               $this->assertLastSqlDb( implode( '; ', [ $sqlSelect, 'BEGIN', $sqlInsert, 'COMMIT' ] ), $dbWeb );
-       }
-
-       public static function provideInsertSelect() {
-               return [
-                       [
-                               [
-                                       'destTable' => 'insert_table',
-                                       'srcTable' => 'select_table',
-                                       'varMap' => [ 'field_insert' => 'field_select', 'field' => 'field2' ],
-                                       'conds' => '*',
-                               ],
-                               "INSERT INTO insert_table " .
-                                       "(field_insert,field) " .
-                                       "SELECT field_select,field2 " .
-                                       "FROM select_table",
-                               "SELECT field_select AS field_insert,field2 AS field " .
-                               "FROM select_table      FOR UPDATE",
-                               "INSERT INTO insert_table (field_insert,field) VALUES ('0','1')"
-                       ],
-                       [
-                               [
-                                       'destTable' => 'insert_table',
-                                       'srcTable' => 'select_table',
-                                       'varMap' => [ 'field_insert' => 'field_select', 'field' => 'field2' ],
-                                       'conds' => [ 'field' => 2 ],
-                               ],
-                               "INSERT INTO insert_table " .
-                                       "(field_insert,field) " .
-                                       "SELECT field_select,field2 " .
-                                       "FROM select_table " .
-                                       "WHERE field = '2'",
-                               "SELECT field_select AS field_insert,field2 AS field FROM " .
-                               "select_table WHERE field = '2'   FOR UPDATE",
-                               "INSERT INTO insert_table (field_insert,field) VALUES ('0','1')"
-                       ],
-                       [
-                               [
-                                       'destTable' => 'insert_table',
-                                       'srcTable' => 'select_table',
-                                       'varMap' => [ 'field_insert' => 'field_select', 'field' => 'field2' ],
-                                       'conds' => [ 'field' => 2 ],
-                                       'insertOptions' => 'IGNORE',
-                                       'selectOptions' => [ 'ORDER BY' => 'field' ],
-                               ],
-                               "INSERT IGNORE INTO insert_table " .
-                                       "(field_insert,field) " .
-                                       "SELECT field_select,field2 " .
-                                       "FROM select_table " .
-                                       "WHERE field = '2' " .
-                                       "ORDER BY field",
-                               "SELECT field_select AS field_insert,field2 AS field " .
-                               "FROM select_table WHERE field = '2' ORDER BY field  FOR UPDATE",
-                               "INSERT IGNORE INTO insert_table (field_insert,field) VALUES ('0','1')"
-                       ],
-                       [
-                               [
-                                       'destTable' => 'insert_table',
-                                       'srcTable' => [ 'select_table1', 'select_table2' ],
-                                       'varMap' => [ 'field_insert' => 'field_select', 'field' => 'field2' ],
-                                       'conds' => [ 'field' => 2 ],
-                                       'insertOptions' => [ 'NO_AUTO_COLUMNS' ],
-                                       'selectOptions' => [ 'ORDER BY' => 'field', 'FORCE INDEX' => [ 'select_table1' => 'index1' ] ],
-                                       'selectJoinConds' => [
-                                               'select_table2' => [ 'LEFT JOIN', [ 'select_table1.foo = select_table2.bar' ] ],
-                                       ],
-                               ],
-                               "INSERT INTO insert_table " .
-                                       "(field_insert,field) " .
-                                       "SELECT field_select,field2 " .
-                                       "FROM select_table1 LEFT JOIN select_table2 ON ((select_table1.foo = select_table2.bar)) " .
-                                       "WHERE field = '2' " .
-                                       "ORDER BY field",
-                               "SELECT field_select AS field_insert,field2 AS field " .
-                               "FROM select_table1 LEFT JOIN select_table2 ON ((select_table1.foo = select_table2.bar)) " .
-                               "WHERE field = '2' ORDER BY field  FOR UPDATE",
-                               "INSERT INTO insert_table (field_insert,field) VALUES ('0','1')"
-                       ],
-               ];
-       }
-
-       /**
-        * @covers Wikimedia\Rdbms\Database::insertSelect
-        * @covers Wikimedia\Rdbms\Database::nativeInsertSelect
-        */
-       public function testInsertSelectBatching() {
-               $dbWeb = new DatabaseTestHelper( __CLASS__, [ 'cliMode' => false ] );
-               $rows = [];
-               for ( $i = 0; $i <= 25000; $i++ ) {
-                       $rows[] = [ 'field' => $i ];
-               }
-               $dbWeb->forceNextResult( $rows );
-               $dbWeb->insertSelect(
-                       'insert_table',
-                       'select_table',
-                       [ 'field' => 'field2' ],
-                       '*',
-                       __METHOD__
-               );
-               $this->assertLastSqlDb( implode( '; ', [
-                       'SELECT field2 AS field FROM select_table      FOR UPDATE',
-                       'BEGIN',
-                       "INSERT INTO insert_table (field) VALUES ('" . implode( "'),('", range( 0, 9999 ) ) . "')",
-                       "INSERT INTO insert_table (field) VALUES ('" . implode( "'),('", range( 10000, 19999 ) ) . "')",
-                       "INSERT INTO insert_table (field) VALUES ('" . implode( "'),('", range( 20000, 25000 ) ) . "')",
-                       'COMMIT'
-               ] ), $dbWeb );
-       }
-
-       /**
-        * @dataProvider provideReplace
-        * @covers Wikimedia\Rdbms\Database::replace
-        */
-       public function testReplace( $sql, $sqlText ) {
-               $this->database->replace(
-                       $sql['table'],
-                       $sql['uniqueIndexes'],
-                       $sql['rows'],
-                       __METHOD__
-               );
-               $this->assertLastSql( $sqlText );
-       }
-
-       public static function provideReplace() {
-               return [
-                       [
-                               [
-                                       'table' => 'replace_table',
-                                       'uniqueIndexes' => [ 'field' ],
-                                       'rows' => [ 'field' => 'text', 'field2' => 'text2' ],
-                               ],
-                               "BEGIN; DELETE FROM replace_table " .
-                                       "WHERE (field = 'text'); " .
-                                       "INSERT INTO replace_table " .
-                                       "(field,field2) " .
-                                       "VALUES ('text','text2'); COMMIT"
-                       ],
-                       [
-                               [
-                                       'table' => 'module_deps',
-                                       'uniqueIndexes' => [ [ 'md_module', 'md_skin' ] ],
-                                       'rows' => [
-                                               'md_module' => 'module',
-                                               'md_skin' => 'skin',
-                                               'md_deps' => 'deps',
-                                       ],
-                               ],
-                               "BEGIN; DELETE FROM module_deps " .
-                                       "WHERE (md_module = 'module' AND md_skin = 'skin'); " .
-                                       "INSERT INTO module_deps " .
-                                       "(md_module,md_skin,md_deps) " .
-                                       "VALUES ('module','skin','deps'); COMMIT"
-                       ],
-                       [
-                               [
-                                       'table' => 'module_deps',
-                                       'uniqueIndexes' => [ [ 'md_module', 'md_skin' ] ],
-                                       'rows' => [
-                                               [
-                                                       'md_module' => 'module',
-                                                       'md_skin' => 'skin',
-                                                       'md_deps' => 'deps',
-                                               ], [
-                                                       'md_module' => 'module2',
-                                                       'md_skin' => 'skin2',
-                                                       'md_deps' => 'deps2',
-                                               ],
-                                       ],
-                               ],
-                               "BEGIN; DELETE FROM module_deps " .
-                                       "WHERE (md_module = 'module' AND md_skin = 'skin'); " .
-                                       "INSERT INTO module_deps " .
-                                       "(md_module,md_skin,md_deps) " .
-                                       "VALUES ('module','skin','deps'); " .
-                                       "DELETE FROM module_deps " .
-                                       "WHERE (md_module = 'module2' AND md_skin = 'skin2'); " .
-                                       "INSERT INTO module_deps " .
-                                       "(md_module,md_skin,md_deps) " .
-                                       "VALUES ('module2','skin2','deps2'); COMMIT"
-                       ],
-                       [
-                               [
-                                       'table' => 'module_deps',
-                                       'uniqueIndexes' => [ 'md_module', 'md_skin' ],
-                                       'rows' => [
-                                               [
-                                                       'md_module' => 'module',
-                                                       'md_skin' => 'skin',
-                                                       'md_deps' => 'deps',
-                                               ], [
-                                                       'md_module' => 'module2',
-                                                       'md_skin' => 'skin2',
-                                                       'md_deps' => 'deps2',
-                                               ],
-                                       ],
-                               ],
-                               "BEGIN; DELETE FROM module_deps " .
-                                       "WHERE (md_module = 'module') OR (md_skin = 'skin'); " .
-                                       "INSERT INTO module_deps " .
-                                       "(md_module,md_skin,md_deps) " .
-                                       "VALUES ('module','skin','deps'); " .
-                                       "DELETE FROM module_deps " .
-                                       "WHERE (md_module = 'module2') OR (md_skin = 'skin2'); " .
-                                       "INSERT INTO module_deps " .
-                                       "(md_module,md_skin,md_deps) " .
-                                       "VALUES ('module2','skin2','deps2'); COMMIT"
-                       ],
-                       [
-                               [
-                                       'table' => 'module_deps',
-                                       'uniqueIndexes' => [],
-                                       'rows' => [
-                                               'md_module' => 'module',
-                                               'md_skin' => 'skin',
-                                               'md_deps' => 'deps',
-                                       ],
-                               ],
-                               "BEGIN; INSERT INTO module_deps " .
-                                       "(md_module,md_skin,md_deps) " .
-                                       "VALUES ('module','skin','deps'); COMMIT"
-                       ],
-               ];
-       }
-
-       /**
-        * @dataProvider provideNativeReplace
-        * @covers Wikimedia\Rdbms\Database::nativeReplace
-        */
-       public function testNativeReplace( $sql, $sqlText ) {
-               $this->database->nativeReplace(
-                       $sql['table'],
-                       $sql['rows'],
-                       __METHOD__
-               );
-               $this->assertLastSql( $sqlText );
-       }
-
-       public static function provideNativeReplace() {
-               return [
-                       [
-                               [
-                                       'table' => 'replace_table',
-                                       'rows' => [ 'field' => 'text', 'field2' => 'text2' ],
-                               ],
-                               "REPLACE INTO replace_table " .
-                                       "(field,field2) " .
-                                       "VALUES ('text','text2')"
-                       ],
-               ];
-       }
-
-       /**
-        * @dataProvider provideConditional
-        * @covers Wikimedia\Rdbms\Database::conditional
-        */
-       public function testConditional( $sql, $sqlText ) {
-               $this->assertEquals( trim( $this->database->conditional(
-                       $sql['conds'],
-                       $sql['true'],
-                       $sql['false']
-               ) ), $sqlText );
-       }
-
-       public static function provideConditional() {
-               return [
-                       [
-                               [
-                                       'conds' => [ 'field' => 'text' ],
-                                       'true' => 1,
-                                       'false' => 'NULL',
-                               ],
-                               "(CASE WHEN field = 'text' THEN 1 ELSE NULL END)"
-                       ],
-                       [
-                               [
-                                       'conds' => [ 'field' => 'text', 'field2' => 'anothertext' ],
-                                       'true' => 1,
-                                       'false' => 'NULL',
-                               ],
-                               "(CASE WHEN field = 'text' AND field2 = 'anothertext' THEN 1 ELSE NULL END)"
-                       ],
-                       [
-                               [
-                                       'conds' => 'field=1',
-                                       'true' => 1,
-                                       'false' => 'NULL',
-                               ],
-                               "(CASE WHEN field=1 THEN 1 ELSE NULL END)"
-                       ],
-               ];
-       }
-
-       /**
-        * @dataProvider provideBuildConcat
-        * @covers Wikimedia\Rdbms\Database::buildConcat
-        */
-       public function testBuildConcat( $stringList, $sqlText ) {
-               $this->assertEquals( trim( $this->database->buildConcat(
-                       $stringList
-               ) ), $sqlText );
-       }
-
-       public static function provideBuildConcat() {
-               return [
-                       [
-                               [ 'field', 'field2' ],
-                               "CONCAT(field,field2)"
-                       ],
-                       [
-                               [ "'test'", 'field2' ],
-                               "CONCAT('test',field2)"
-                       ],
-               ];
-       }
-
-       /**
-        * @dataProvider provideBuildLike
-        * @covers Wikimedia\Rdbms\Database::buildLike
-        * @covers Wikimedia\Rdbms\Database::escapeLikeInternal
-        */
-       public function testBuildLike( $array, $sqlText ) {
-               $this->assertEquals( trim( $this->database->buildLike(
-                       $array
-               ) ), $sqlText );
-       }
-
-       public static function provideBuildLike() {
-               return [
-                       [
-                               'text',
-                               "LIKE 'text' ESCAPE '`'"
-                       ],
-                       [
-                               [ 'text', new LikeMatch( '%' ) ],
-                               "LIKE 'text%' ESCAPE '`'"
-                       ],
-                       [
-                               [ 'text', new LikeMatch( '%' ), 'text2' ],
-                               "LIKE 'text%text2' ESCAPE '`'"
-                       ],
-                       [
-                               [ 'text', new LikeMatch( '_' ) ],
-                               "LIKE 'text_' ESCAPE '`'"
-                       ],
-                       [
-                               'more_text',
-                               "LIKE 'more`_text' ESCAPE '`'"
-                       ],
-                       [
-                               [ 'C:\\Windows\\', new LikeMatch( '%' ) ],
-                               "LIKE 'C:\\Windows\\%' ESCAPE '`'"
-                       ],
-                       [
-                               [ 'accent`_test`', new LikeMatch( '%' ) ],
-                               "LIKE 'accent```_test``%' ESCAPE '`'"
-                       ],
-               ];
-       }
-
-       /**
-        * @dataProvider provideUnionQueries
-        * @covers Wikimedia\Rdbms\Database::unionQueries
-        */
-       public function testUnionQueries( $sql, $sqlText ) {
-               $this->assertEquals( trim( $this->database->unionQueries(
-                       $sql['sqls'],
-                       $sql['all']
-               ) ), $sqlText );
-       }
-
-       public static function provideUnionQueries() {
-               return [
-                       [
-                               [
-                                       'sqls' => [ 'RAW SQL', 'RAW2SQL' ],
-                                       'all' => true,
-                               ],
-                               "(RAW SQL) UNION ALL (RAW2SQL)"
-                       ],
-                       [
-                               [
-                                       'sqls' => [ 'RAW SQL', 'RAW2SQL' ],
-                                       'all' => false,
-                               ],
-                               "(RAW SQL) UNION (RAW2SQL)"
-                       ],
-                       [
-                               [
-                                       'sqls' => [ 'RAW SQL', 'RAW2SQL', 'RAW3SQL' ],
-                                       'all' => false,
-                               ],
-                               "(RAW SQL) UNION (RAW2SQL) UNION (RAW3SQL)"
-                       ],
-               ];
-       }
-
-       /**
-        * @dataProvider provideUnionConditionPermutations
-        * @covers Wikimedia\Rdbms\Database::unionConditionPermutations
-        */
-       public function testUnionConditionPermutations( $params, $expect ) {
-               if ( isset( $params['unionSupportsOrderAndLimit'] ) ) {
-                       $this->database->setUnionSupportsOrderAndLimit( $params['unionSupportsOrderAndLimit'] );
-               }
-
-               $sql = trim( $this->database->unionConditionPermutations(
-                       $params['table'],
-                       $params['vars'],
-                       $params['permute_conds'],
-                       $params['extra_conds'] ?? '',
-                       'FNAME',
-                       $params['options'] ?? [],
-                       $params['join_conds'] ?? []
-               ) );
-               $this->assertEquals( $expect, $sql );
-       }
-
-       public static function provideUnionConditionPermutations() {
-               // phpcs:disable Generic.Files.LineLength
-               return [
-                       [
-                               [
-                                       'table' => [ 'table1', 'table2' ],
-                                       'vars' => [ 'field1', 'alias' => 'field2' ],
-                                       'permute_conds' => [
-                                               'field3' => [ 1, 2, 3 ],
-                                               'duplicates' => [ 4, 5, 4 ],
-                                               'empty' => [],
-                                               'single' => [ 0 ],
-                                       ],
-                                       'extra_conds' => 'table2.bar > 23',
-                                       'options' => [
-                                               'ORDER BY' => [ 'field1', 'alias' ],
-                                               'INNER ORDER BY' => [ 'field1', 'field2' ],
-                                               'LIMIT' => 100,
-                                       ],
-                                       'join_conds' => [
-                                               'table2' => [ 'JOIN', 'table1.foo_id = table2.foo_id' ],
-                                       ],
-                               ],
-                               "(SELECT  field1,field2 AS alias  FROM table1 JOIN table2 ON ((table1.foo_id = table2.foo_id))   WHERE field3 = '1' AND duplicates = '4' AND single = '0' AND (table2.bar > 23)  ORDER BY field1,field2 LIMIT 100  ) UNION ALL " .
-                               "(SELECT  field1,field2 AS alias  FROM table1 JOIN table2 ON ((table1.foo_id = table2.foo_id))   WHERE field3 = '1' AND duplicates = '5' AND single = '0' AND (table2.bar > 23)  ORDER BY field1,field2 LIMIT 100  ) UNION ALL " .
-                               "(SELECT  field1,field2 AS alias  FROM table1 JOIN table2 ON ((table1.foo_id = table2.foo_id))   WHERE field3 = '2' AND duplicates = '4' AND single = '0' AND (table2.bar > 23)  ORDER BY field1,field2 LIMIT 100  ) UNION ALL " .
-                               "(SELECT  field1,field2 AS alias  FROM table1 JOIN table2 ON ((table1.foo_id = table2.foo_id))   WHERE field3 = '2' AND duplicates = '5' AND single = '0' AND (table2.bar > 23)  ORDER BY field1,field2 LIMIT 100  ) UNION ALL " .
-                               "(SELECT  field1,field2 AS alias  FROM table1 JOIN table2 ON ((table1.foo_id = table2.foo_id))   WHERE field3 = '3' AND duplicates = '4' AND single = '0' AND (table2.bar > 23)  ORDER BY field1,field2 LIMIT 100  ) UNION ALL " .
-                               "(SELECT  field1,field2 AS alias  FROM table1 JOIN table2 ON ((table1.foo_id = table2.foo_id))   WHERE field3 = '3' AND duplicates = '5' AND single = '0' AND (table2.bar > 23)  ORDER BY field1,field2 LIMIT 100  ) " .
-                               "ORDER BY field1,alias LIMIT 100"
-                       ],
-                       [
-                               [
-                                       'table' => 'foo',
-                                       'vars' => [ 'foo_id' ],
-                                       'permute_conds' => [
-                                               'bar' => [ 1, 2, 3 ],
-                                       ],
-                                       'extra_conds' => [ 'baz' => null ],
-                                       'options' => [
-                                               'NOTALL',
-                                               'ORDER BY' => [ 'foo_id' ],
-                                               'LIMIT' => 25,
-                                       ],
-                               ],
-                               "(SELECT  foo_id  FROM foo    WHERE bar = '1' AND baz IS NULL  ORDER BY foo_id LIMIT 25  ) UNION " .
-                               "(SELECT  foo_id  FROM foo    WHERE bar = '2' AND baz IS NULL  ORDER BY foo_id LIMIT 25  ) UNION " .
-                               "(SELECT  foo_id  FROM foo    WHERE bar = '3' AND baz IS NULL  ORDER BY foo_id LIMIT 25  ) " .
-                               "ORDER BY foo_id LIMIT 25"
-                       ],
-                       [
-                               [
-                                       'table' => 'foo',
-                                       'vars' => [ 'foo_id' ],
-                                       'permute_conds' => [
-                                               'bar' => [ 1, 2, 3 ],
-                                       ],
-                                       'extra_conds' => [ 'baz' => null ],
-                                       'options' => [
-                                               'NOTALL' => true,
-                                               'ORDER BY' => [ 'foo_id' ],
-                                               'LIMIT' => 25,
-                                       ],
-                                       'unionSupportsOrderAndLimit' => false,
-                               ],
-                               "(SELECT  foo_id  FROM foo    WHERE bar = '1' AND baz IS NULL  ) UNION " .
-                               "(SELECT  foo_id  FROM foo    WHERE bar = '2' AND baz IS NULL  ) UNION " .
-                               "(SELECT  foo_id  FROM foo    WHERE bar = '3' AND baz IS NULL  ) " .
-                               "ORDER BY foo_id LIMIT 25"
-                       ],
-                       [
-                               [
-                                       'table' => 'foo',
-                                       'vars' => [ 'foo_id' ],
-                                       'permute_conds' => [],
-                                       'extra_conds' => [ 'baz' => null ],
-                                       'options' => [
-                                               'ORDER BY' => [ 'foo_id' ],
-                                               'LIMIT' => 25,
-                                       ],
-                               ],
-                               "SELECT  foo_id  FROM foo    WHERE baz IS NULL  ORDER BY foo_id LIMIT 25"
-                       ],
-                       [
-                               [
-                                       'table' => 'foo',
-                                       'vars' => [ 'foo_id' ],
-                                       'permute_conds' => [
-                                               'bar' => [],
-                                       ],
-                                       'extra_conds' => [ 'baz' => null ],
-                                       'options' => [
-                                               'ORDER BY' => [ 'foo_id' ],
-                                               'LIMIT' => 25,
-                                       ],
-                               ],
-                               "SELECT  foo_id  FROM foo    WHERE baz IS NULL  ORDER BY foo_id LIMIT 25"
-                       ],
-                       [
-                               [
-                                       'table' => 'foo',
-                                       'vars' => [ 'foo_id' ],
-                                       'permute_conds' => [
-                                               'bar' => [ 1 ],
-                                       ],
-                                       'options' => [
-                                               'ORDER BY' => [ 'foo_id' ],
-                                               'LIMIT' => 25,
-                                               'OFFSET' => 150,
-                                       ],
-                               ],
-                               "SELECT  foo_id  FROM foo    WHERE bar = '1'  ORDER BY foo_id LIMIT 150,25"
-                       ],
-                       [
-                               [
-                                       'table' => 'foo',
-                                       'vars' => [ 'foo_id' ],
-                                       'permute_conds' => [],
-                                       'extra_conds' => [ 'baz' => null ],
-                                       'options' => [
-                                               'ORDER BY' => [ 'foo_id' ],
-                                               'LIMIT' => 25,
-                                               'OFFSET' => 150,
-                                               'INNER ORDER BY' => [ 'bar_id' ],
-                                       ],
-                               ],
-                               "(SELECT  foo_id  FROM foo    WHERE baz IS NULL  ORDER BY bar_id LIMIT 175  ) ORDER BY foo_id LIMIT 150,25"
-                       ],
-                       [
-                               [
-                                       'table' => 'foo',
-                                       'vars' => [ 'foo_id' ],
-                                       'permute_conds' => [],
-                                       'extra_conds' => [ 'baz' => null ],
-                                       'options' => [
-                                               'ORDER BY' => [ 'foo_id' ],
-                                               'LIMIT' => 25,
-                                               'OFFSET' => 150,
-                                               'INNER ORDER BY' => [ 'bar_id' ],
-                                       ],
-                                       'unionSupportsOrderAndLimit' => false,
-                               ],
-                               "SELECT  foo_id  FROM foo    WHERE baz IS NULL  ORDER BY foo_id LIMIT 150,25"
-                       ],
-               ];
-               // phpcs:enable
-       }
-
-       /**
-        * @covers Wikimedia\Rdbms\Database::commit
-        * @covers Wikimedia\Rdbms\Database::doCommit
-        */
-       public function testTransactionCommit() {
-               $this->database->begin( __METHOD__ );
-               $this->database->commit( __METHOD__ );
-               $this->assertLastSql( 'BEGIN; COMMIT' );
-       }
-
-       /**
-        * @covers Wikimedia\Rdbms\Database::rollback
-        * @covers Wikimedia\Rdbms\Database::doRollback
-        */
-       public function testTransactionRollback() {
-               $this->database->begin( __METHOD__ );
-               $this->database->rollback( __METHOD__ );
-               $this->assertLastSql( 'BEGIN; ROLLBACK' );
-       }
-
-       /**
-        * @covers Wikimedia\Rdbms\Database::dropTable
-        */
-       public function testDropTable() {
-               $this->database->setExistingTables( [ 'table' ] );
-               $this->database->dropTable( 'table', __METHOD__ );
-               $this->assertLastSql( 'DROP TABLE table CASCADE' );
-       }
-
-       /**
-        * @covers Wikimedia\Rdbms\Database::dropTable
-        */
-       public function testDropNonExistingTable() {
-               $this->assertFalse(
-                       $this->database->dropTable( 'non_existing', __METHOD__ )
-               );
-       }
-
-       /**
-        * @dataProvider provideMakeList
-        * @covers Wikimedia\Rdbms\Database::makeList
-        */
-       public function testMakeList( $list, $mode, $sqlText ) {
-               $this->assertEquals( trim( $this->database->makeList(
-                       $list, $mode
-               ) ), $sqlText );
-       }
-
-       public static function provideMakeList() {
-               return [
-                       [
-                               [ 'value', 'value2' ],
-                               LIST_COMMA,
-                               "'value','value2'"
-                       ],
-                       [
-                               [ 'field', 'field2' ],
-                               LIST_NAMES,
-                               "field,field2"
-                       ],
-                       [
-                               [ 'field' => 'value', 'field2' => 'value2' ],
-                               LIST_AND,
-                               "field = 'value' AND field2 = 'value2'"
-                       ],
-                       [
-                               [ 'field' => null, "field2 != 'value2'" ],
-                               LIST_AND,
-                               "field IS NULL AND (field2 != 'value2')"
-                       ],
-                       [
-                               [ 'field' => [ 'value', null, 'value2' ], 'field2' => 'value2' ],
-                               LIST_AND,
-                               "(field IN ('value','value2')  OR field IS NULL) AND field2 = 'value2'"
-                       ],
-                       [
-                               [ 'field' => [ null ], 'field2' => null ],
-                               LIST_AND,
-                               "field IS NULL AND field2 IS NULL"
-                       ],
-                       [
-                               [ 'field' => 'value', 'field2' => 'value2' ],
-                               LIST_OR,
-                               "field = 'value' OR field2 = 'value2'"
-                       ],
-                       [
-                               [ 'field' => 'value', 'field2' => null ],
-                               LIST_OR,
-                               "field = 'value' OR field2 IS NULL"
-                       ],
-                       [
-                               [ 'field' => [ 'value', 'value2' ], 'field2' => [ 'value' ] ],
-                               LIST_OR,
-                               "field IN ('value','value2')  OR field2 = 'value'"
-                       ],
-                       [
-                               [ 'field' => [ null, 'value', null, 'value2' ], "field2 != 'value2'" ],
-                               LIST_OR,
-                               "(field IN ('value','value2')  OR field IS NULL) OR (field2 != 'value2')"
-                       ],
-                       [
-                               [ 'field' => 'value', 'field2' => 'value2' ],
-                               LIST_SET,
-                               "field = 'value',field2 = 'value2'"
-                       ],
-                       [
-                               [ 'field' => 'value', 'field2' => null ],
-                               LIST_SET,
-                               "field = 'value',field2 = NULL"
-                       ],
-                       [
-                               [ 'field' => 'value', "field2 != 'value2'" ],
-                               LIST_SET,
-                               "field = 'value',field2 != 'value2'"
-                       ],
-               ];
-       }
-
-       /**
-        * @covers Wikimedia\Rdbms\Database::registerTempTableWrite
-        */
-       public function testSessionTempTables() {
-               $temp1 = $this->database->tableName( 'tmp_table_1' );
-               $temp2 = $this->database->tableName( 'tmp_table_2' );
-               $temp3 = $this->database->tableName( 'tmp_table_3' );
-
-               $this->database->query( "CREATE TEMPORARY TABLE $temp1 LIKE orig_tbl", __METHOD__ );
-               $this->database->query( "CREATE TEMPORARY TABLE $temp2 LIKE orig_tbl", __METHOD__ );
-               $this->database->query( "CREATE TEMPORARY TABLE $temp3 LIKE orig_tbl", __METHOD__ );
-
-               $this->assertTrue( $this->database->tableExists( "tmp_table_1", __METHOD__ ) );
-               $this->assertTrue( $this->database->tableExists( "tmp_table_2", __METHOD__ ) );
-               $this->assertTrue( $this->database->tableExists( "tmp_table_3", __METHOD__ ) );
-
-               $this->database->dropTable( 'tmp_table_1', __METHOD__ );
-               $this->database->dropTable( 'tmp_table_2', __METHOD__ );
-               $this->database->dropTable( 'tmp_table_3', __METHOD__ );
-
-               $this->assertFalse( $this->database->tableExists( "tmp_table_1", __METHOD__ ) );
-               $this->assertFalse( $this->database->tableExists( "tmp_table_2", __METHOD__ ) );
-               $this->assertFalse( $this->database->tableExists( "tmp_table_3", __METHOD__ ) );
-
-               $this->database->query( "CREATE TEMPORARY TABLE tmp_table_1 LIKE orig_tbl", __METHOD__ );
-               $this->database->query( "CREATE TEMPORARY TABLE 'tmp_table_2' LIKE orig_tbl", __METHOD__ );
-               $this->database->query( "CREATE TEMPORARY TABLE `tmp_table_3` LIKE orig_tbl", __METHOD__ );
-
-               $this->assertTrue( $this->database->tableExists( "tmp_table_1", __METHOD__ ) );
-               $this->assertTrue( $this->database->tableExists( "tmp_table_2", __METHOD__ ) );
-               $this->assertTrue( $this->database->tableExists( "tmp_table_3", __METHOD__ ) );
-
-               $this->database->query( "DROP TEMPORARY TABLE tmp_table_1 LIKE orig_tbl", __METHOD__ );
-               $this->database->query( "DROP TEMPORARY TABLE 'tmp_table_2' LIKE orig_tbl", __METHOD__ );
-               $this->database->query( "DROP TABLE `tmp_table_3` LIKE orig_tbl", __METHOD__ );
-
-               $this->assertFalse( $this->database->tableExists( "tmp_table_1", __METHOD__ ) );
-               $this->assertFalse( $this->database->tableExists( "tmp_table_2", __METHOD__ ) );
-               $this->assertFalse( $this->database->tableExists( "tmp_table_3", __METHOD__ ) );
-       }
-
-       public function provideBuildSubstring() {
-               yield [ 'someField', 1, 2, 'SUBSTRING(someField FROM 1 FOR 2)' ];
-               yield [ 'someField', 1, null, 'SUBSTRING(someField FROM 1)' ];
-       }
-
-       /**
-        * @covers Wikimedia\Rdbms\Database::buildSubstring
-        * @dataProvider provideBuildSubstring
-        */
-       public function testBuildSubstring( $input, $start, $length, $expected ) {
-               $output = $this->database->buildSubstring( $input, $start, $length );
-               $this->assertSame( $expected, $output );
-       }
-
-       public function provideBuildSubstring_invalidParams() {
-               yield [ -1, 1 ];
-               yield [ 1, -1 ];
-               yield [ 1, 'foo' ];
-               yield [ 'foo', 1 ];
-               yield [ null, 1 ];
-               yield [ 0, 1 ];
-       }
-
-       /**
-        * @covers Wikimedia\Rdbms\Database::buildSubstring
-        * @covers Wikimedia\Rdbms\Database::assertBuildSubstringParams
-        * @dataProvider provideBuildSubstring_invalidParams
-        */
-       public function testBuildSubstring_invalidParams( $start, $length ) {
-               $this->setExpectedException( InvalidArgumentException::class );
-               $this->database->buildSubstring( 'foo', $start, $length );
-       }
-
-       /**
-        * @covers \Wikimedia\Rdbms\Database::buildIntegerCast
-        */
-       public function testBuildIntegerCast() {
-               $output = $this->database->buildIntegerCast( 'fieldName' );
-               $this->assertSame( 'CAST( fieldName AS INTEGER )', $output );
-       }
-
-       /**
-        * @covers \Wikimedia\Rdbms\Database::doSavepoint
-        * @covers \Wikimedia\Rdbms\Database::doReleaseSavepoint
-        * @covers \Wikimedia\Rdbms\Database::doRollbackToSavepoint
-        * @covers \Wikimedia\Rdbms\Database::startAtomic
-        * @covers \Wikimedia\Rdbms\Database::endAtomic
-        * @covers \Wikimedia\Rdbms\Database::cancelAtomic
-        * @covers \Wikimedia\Rdbms\Database::doAtomicSection
-        */
-       public function testAtomicSections() {
-               $this->database->startAtomic( __METHOD__ );
-               $this->database->endAtomic( __METHOD__ );
-               $this->assertLastSql( 'BEGIN; COMMIT' );
-
-               $this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
-               $this->database->cancelAtomic( __METHOD__ );
-               $this->assertLastSql( 'BEGIN; ROLLBACK' );
-
-               $this->database->begin( __METHOD__ );
-               $this->database->startAtomic( __METHOD__ );
-               $this->database->endAtomic( __METHOD__ );
-               $this->database->commit( __METHOD__ );
-               $this->assertLastSql( 'BEGIN; COMMIT' );
-
-               $this->database->begin( __METHOD__ );
-               $this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
-               $this->database->endAtomic( __METHOD__ );
-               $this->database->commit( __METHOD__ );
-               // phpcs:ignore Generic.Files.LineLength
-               $this->assertLastSql( 'BEGIN; SAVEPOINT wikimedia_rdbms_atomic1; RELEASE SAVEPOINT wikimedia_rdbms_atomic1; COMMIT' );
-
-               $this->database->begin( __METHOD__ );
-               $this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
-               $this->database->cancelAtomic( __METHOD__ );
-               $this->database->commit( __METHOD__ );
-               // phpcs:ignore Generic.Files.LineLength
-               $this->assertLastSql( 'BEGIN; SAVEPOINT wikimedia_rdbms_atomic1; ROLLBACK TO SAVEPOINT wikimedia_rdbms_atomic1; COMMIT' );
-
-               $this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
-               $this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
-               $this->database->cancelAtomic( __METHOD__ );
-               $this->database->endAtomic( __METHOD__ );
-               // phpcs:ignore Generic.Files.LineLength
-               $this->assertLastSql( 'BEGIN; SAVEPOINT wikimedia_rdbms_atomic1; ROLLBACK TO SAVEPOINT wikimedia_rdbms_atomic1; COMMIT' );
-
-               $noOpCallack = function () {
-               };
-
-               $this->database->doAtomicSection( __METHOD__, $noOpCallack, IDatabase::ATOMIC_CANCELABLE );
-               $this->assertLastSql( 'BEGIN; COMMIT' );
-
-               $this->database->doAtomicSection( __METHOD__, $noOpCallack );
-               $this->assertLastSql( 'BEGIN; COMMIT' );
-
-               $this->database->begin( __METHOD__ );
-               $this->database->doAtomicSection( __METHOD__, $noOpCallack, IDatabase::ATOMIC_CANCELABLE );
-               $this->database->rollback( __METHOD__ );
-               // phpcs:ignore Generic.Files.LineLength
-               $this->assertLastSql( 'BEGIN; SAVEPOINT wikimedia_rdbms_atomic1; RELEASE SAVEPOINT wikimedia_rdbms_atomic1; ROLLBACK' );
-
-               $fname = __METHOD__;
-               $triggerMap = [
-                       '-' => '-',
-                       IDatabase::TRIGGER_COMMIT => 'tCommit',
-                       IDatabase::TRIGGER_ROLLBACK => 'tRollback'
-               ];
-               $pcCallback = function ( IDatabase $db ) use ( $fname ) {
-                       $this->database->query( "SELECT 0", $fname );
-               };
-               $callback1 = function ( $trigger = '-' ) use ( $fname, $triggerMap ) {
-                       $this->database->query( "SELECT 1, {$triggerMap[$trigger]} AS t", $fname );
-               };
-               $callback2 = function ( $trigger = '-' ) use ( $fname, $triggerMap ) {
-                       $this->database->query( "SELECT 2, {$triggerMap[$trigger]} AS t", $fname );
-               };
-               $callback3 = function ( $trigger = '-' ) use ( $fname, $triggerMap ) {
-                       $this->database->query( "SELECT 3, {$triggerMap[$trigger]} AS t", $fname );
-               };
-
-               $this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
-               $this->database->onTransactionPreCommitOrIdle( $pcCallback, __METHOD__ );
-               $this->database->cancelAtomic( __METHOD__ );
-               $this->assertLastSql( 'BEGIN; ROLLBACK' );
-
-               $this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
-               $this->database->onTransactionCommitOrIdle( $callback1, __METHOD__ );
-               $this->database->cancelAtomic( __METHOD__ );
-               $this->assertLastSql( 'BEGIN; ROLLBACK' );
-
-               $this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
-               $this->database->onTransactionResolution( $callback1, __METHOD__ );
-               $this->database->cancelAtomic( __METHOD__ );
-               $this->assertLastSql( 'BEGIN; ROLLBACK; SELECT 1, tRollback AS t' );
-
-               $this->database->startAtomic( __METHOD__ . '_outer' );
-               $this->database->onTransactionPreCommitOrIdle( $pcCallback, __METHOD__ );
-               $this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
-               $this->database->onTransactionPreCommitOrIdle( $pcCallback, __METHOD__ );
-               $this->database->cancelAtomic( __METHOD__ );
-               $this->database->onTransactionPreCommitOrIdle( $pcCallback, __METHOD__ );
-               $this->database->endAtomic( __METHOD__ . '_outer' );
-               $this->assertLastSql( implode( "; ", [
-                       'BEGIN',
-                       'SAVEPOINT wikimedia_rdbms_atomic1',
-                       'ROLLBACK TO SAVEPOINT wikimedia_rdbms_atomic1',
-                       'SELECT 0',
-                       'SELECT 0',
-                       'COMMIT'
-               ] ) );
-
-               $this->database->startAtomic( __METHOD__ . '_outer' );
-               $this->database->onTransactionCommitOrIdle( $callback1, __METHOD__ );
-               $this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
-               $this->database->onTransactionCommitOrIdle( $callback2, __METHOD__ );
-               $this->database->cancelAtomic( __METHOD__ );
-               $this->database->onTransactionCommitOrIdle( $callback3, __METHOD__ );
-               $this->database->endAtomic( __METHOD__ . '_outer' );
-               $this->assertLastSql( implode( "; ", [
-                       'BEGIN',
-                       'SAVEPOINT wikimedia_rdbms_atomic1',
-                       'ROLLBACK TO SAVEPOINT wikimedia_rdbms_atomic1',
-                       'COMMIT',
-                       'SELECT 1, tCommit AS t',
-                       'SELECT 3, tCommit AS t'
-               ] ) );
-
-               $this->database->startAtomic( __METHOD__ . '_outer' );
-               $this->database->onTransactionResolution( $callback1, __METHOD__ );
-               $this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
-               $this->database->onTransactionResolution( $callback2, __METHOD__ );
-               $this->database->cancelAtomic( __METHOD__ );
-               $this->database->onTransactionResolution( $callback3, __METHOD__ );
-               $this->database->endAtomic( __METHOD__ . '_outer' );
-               $this->assertLastSql( implode( "; ", [
-                       'BEGIN',
-                       'SAVEPOINT wikimedia_rdbms_atomic1',
-                       'ROLLBACK TO SAVEPOINT wikimedia_rdbms_atomic1',
-                       'COMMIT',
-                       'SELECT 1, tCommit AS t',
-                       'SELECT 2, tRollback AS t',
-                       'SELECT 3, tCommit AS t'
-               ] ) );
-
-               $makeCallback = function ( $id ) use ( $fname, $triggerMap ) {
-                       return function ( $trigger = '-' ) use ( $id, $fname, $triggerMap ) {
-                               $this->database->query( "SELECT $id, {$triggerMap[$trigger]} AS t", $fname );
-                       };
-               };
-
-               $this->database->startAtomic( __METHOD__ . '_outer' );
-               $this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
-               $this->database->onTransactionResolution( $makeCallback( 1 ), __METHOD__ );
-               $this->database->cancelAtomic( __METHOD__ );
-               $this->database->endAtomic( __METHOD__ . '_outer' );
-               $this->assertLastSql( implode( "; ", [
-                       'BEGIN',
-                       'SAVEPOINT wikimedia_rdbms_atomic1',
-                       'ROLLBACK TO SAVEPOINT wikimedia_rdbms_atomic1',
-                       'COMMIT',
-                       'SELECT 1, tRollback AS t'
-               ] ) );
-
-               $this->database->startAtomic( __METHOD__ . '_level1', IDatabase::ATOMIC_CANCELABLE );
-               $this->database->onTransactionResolution( $makeCallback( 1 ), __METHOD__ );
-               $this->database->startAtomic( __METHOD__ . '_level2' );
-               $this->database->startAtomic( __METHOD__ . '_level3', IDatabase::ATOMIC_CANCELABLE );
-               $this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
-               $this->database->onTransactionResolution( $makeCallback( 2 ), __METHOD__ );
-               $this->database->endAtomic( __METHOD__ );
-               $this->database->onTransactionResolution( $makeCallback( 3 ), __METHOD__ );
-               $this->database->cancelAtomic( __METHOD__ . '_level3' );
-               $this->database->endAtomic( __METHOD__ . '_level2' );
-               $this->database->onTransactionResolution( $makeCallback( 4 ), __METHOD__ );
-               $this->database->endAtomic( __METHOD__ . '_level1' );
-               $this->assertLastSql( implode( "; ", [
-                       'BEGIN',
-                       'SAVEPOINT wikimedia_rdbms_atomic1',
-                       'SAVEPOINT wikimedia_rdbms_atomic2',
-                       'RELEASE SAVEPOINT wikimedia_rdbms_atomic2',
-                       'ROLLBACK TO SAVEPOINT wikimedia_rdbms_atomic1',
-                       'COMMIT; SELECT 1, tCommit AS t',
-                       'SELECT 2, tRollback AS t',
-                       'SELECT 3, tRollback AS t',
-                       'SELECT 4, tCommit AS t'
-               ] ) );
-       }
-
-       /**
-        * @covers \Wikimedia\Rdbms\Database::doSavepoint
-        * @covers \Wikimedia\Rdbms\Database::doReleaseSavepoint
-        * @covers \Wikimedia\Rdbms\Database::doRollbackToSavepoint
-        * @covers \Wikimedia\Rdbms\Database::startAtomic
-        * @covers \Wikimedia\Rdbms\Database::endAtomic
-        * @covers \Wikimedia\Rdbms\Database::cancelAtomic
-        * @covers \Wikimedia\Rdbms\Database::doAtomicSection
-        */
-       public function testAtomicSectionsRecovery() {
-               $this->database->begin( __METHOD__ );
-               try {
-                       $this->database->doAtomicSection(
-                               __METHOD__,
-                               function () {
-                                       $this->database->startAtomic( 'inner_func1' );
-                                       $this->database->startAtomic( 'inner_func2' );
-
-                                       throw new RuntimeException( 'Test exception' );
-                               },
-                               IDatabase::ATOMIC_CANCELABLE
-                       );
-                       $this->fail( 'Expected exception not thrown' );
-               } catch ( RuntimeException $ex ) {
-                       $this->assertSame( 'Test exception', $ex->getMessage() );
-               }
-               $this->database->commit( __METHOD__ );
-               // phpcs:ignore Generic.Files.LineLength
-               $this->assertLastSql( 'BEGIN; SAVEPOINT wikimedia_rdbms_atomic1; ROLLBACK TO SAVEPOINT wikimedia_rdbms_atomic1; COMMIT' );
-
-               $this->database->begin( __METHOD__ );
-               try {
-                       $this->database->doAtomicSection(
-                               __METHOD__,
-                               function () {
-                                       throw new RuntimeException( 'Test exception' );
-                               }
-                       );
-                       $this->fail( 'Test exception not thrown' );
-               } catch ( RuntimeException $ex ) {
-                       $this->assertSame( 'Test exception', $ex->getMessage() );
-               }
-               try {
-                       $this->database->commit( __METHOD__ );
-                       $this->fail( 'Test exception not thrown' );
-               } catch ( DBTransactionError $ex ) {
-                       $this->assertSame(
-                               'Cannot execute query from ' . __METHOD__ . ' while transaction status is ERROR.',
-                               $ex->getMessage()
-                       );
-               }
-               $this->database->rollback( __METHOD__ );
-               $this->assertLastSql( 'BEGIN; ROLLBACK' );
-       }
-
-       /**
-        * @covers \Wikimedia\Rdbms\Database::doSavepoint
-        * @covers \Wikimedia\Rdbms\Database::doReleaseSavepoint
-        * @covers \Wikimedia\Rdbms\Database::doRollbackToSavepoint
-        * @covers \Wikimedia\Rdbms\Database::startAtomic
-        * @covers \Wikimedia\Rdbms\Database::endAtomic
-        * @covers \Wikimedia\Rdbms\Database::cancelAtomic
-        * @covers \Wikimedia\Rdbms\Database::doAtomicSection
-        */
-       public function testAtomicSectionsCallbackCancellation() {
-               $fname = __METHOD__;
-               $callback1Called = null;
-               $callback1 = function ( $trigger = '-' ) use ( $fname, &$callback1Called ) {
-                       $callback1Called = $trigger;
-                       $this->database->query( "SELECT 1", $fname );
-               };
-               $callback2Called = null;
-               $callback2 = function ( $trigger = '-' ) use ( $fname, &$callback2Called ) {
-                       $callback2Called = $trigger;
-                       $this->database->query( "SELECT 2", $fname );
-               };
-               $callback3Called = null;
-               $callback3 = function ( $trigger = '-' ) use ( $fname, &$callback3Called ) {
-                       $callback3Called = $trigger;
-                       $this->database->query( "SELECT 3", $fname );
-               };
-
-               $this->database->startAtomic( __METHOD__ . '_outer' );
-               $this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
-               $this->database->startAtomic( __METHOD__ . '_inner' );
-               $this->database->onTransactionCommitOrIdle( $callback1, __METHOD__ );
-               $this->database->onTransactionPreCommitOrIdle( $callback2, __METHOD__ );
-               $this->database->onTransactionResolution( $callback3, __METHOD__ );
-               $this->database->endAtomic( __METHOD__ . '_inner' );
-               $this->database->cancelAtomic( __METHOD__ );
-               $this->database->endAtomic( __METHOD__ . '_outer' );
-               $this->assertNull( $callback1Called );
-               $this->assertNull( $callback2Called );
-               $this->assertEquals( IDatabase::TRIGGER_ROLLBACK, $callback3Called );
-               // phpcs:ignore Generic.Files.LineLength
-               $this->assertLastSql( 'BEGIN; SAVEPOINT wikimedia_rdbms_atomic1; ROLLBACK TO SAVEPOINT wikimedia_rdbms_atomic1; COMMIT; SELECT 3' );
-
-               $callback1Called = null;
-               $callback2Called = null;
-               $callback3Called = null;
-               $this->database->startAtomic( __METHOD__ . '_outer' );
-               $this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
-               $this->database->startAtomic( __METHOD__ . '_inner', IDatabase::ATOMIC_CANCELABLE );
-               $this->database->onTransactionCommitOrIdle( $callback1, __METHOD__ );
-               $this->database->onTransactionPreCommitOrIdle( $callback2, __METHOD__ );
-               $this->database->onTransactionResolution( $callback3, __METHOD__ );
-               $this->database->endAtomic( __METHOD__ . '_inner' );
-               $this->database->cancelAtomic( __METHOD__ );
-               $this->database->endAtomic( __METHOD__ . '_outer' );
-               $this->assertNull( $callback1Called );
-               $this->assertNull( $callback2Called );
-               $this->assertEquals( IDatabase::TRIGGER_ROLLBACK, $callback3Called );
-               // phpcs:ignore Generic.Files.LineLength
-               $this->assertLastSql( 'BEGIN; SAVEPOINT wikimedia_rdbms_atomic1; SAVEPOINT wikimedia_rdbms_atomic2; RELEASE SAVEPOINT wikimedia_rdbms_atomic2; ROLLBACK TO SAVEPOINT wikimedia_rdbms_atomic1; COMMIT; SELECT 3' );
-
-               $callback1Called = null;
-               $callback2Called = null;
-               $callback3Called = null;
-               $this->database->startAtomic( __METHOD__ . '_outer' );
-               $atomicId = $this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
-               $this->database->startAtomic( __METHOD__ . '_inner' );
-               $this->database->onTransactionCommitOrIdle( $callback1, __METHOD__ );
-               $this->database->onTransactionPreCommitOrIdle( $callback2, __METHOD__ );
-               $this->database->onTransactionResolution( $callback3, __METHOD__ );
-               $this->database->cancelAtomic( __METHOD__, $atomicId );
-               $this->database->endAtomic( __METHOD__ . '_outer' );
-               $this->assertNull( $callback1Called );
-               $this->assertNull( $callback2Called );
-               $this->assertEquals( IDatabase::TRIGGER_ROLLBACK, $callback3Called );
-
-               $callback1Called = null;
-               $callback2Called = null;
-               $callback3Called = null;
-               $this->database->startAtomic( __METHOD__ . '_outer' );
-               $atomicId = $this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
-               $this->database->startAtomic( __METHOD__ . '_inner' );
-               $this->database->onTransactionCommitOrIdle( $callback1, __METHOD__ );
-               $this->database->onTransactionPreCommitOrIdle( $callback2, __METHOD__ );
-               $this->database->onTransactionResolution( $callback3, __METHOD__ );
-               try {
-                       $this->database->cancelAtomic( __METHOD__ . '_X', $atomicId );
-               } catch ( DBUnexpectedError $e ) {
-                       $m = __METHOD__;
-                       $this->assertSame(
-                               "Invalid atomic section ended (got {$m}_X but expected {$m}).",
-                               $e->getMessage()
-                       );
-               }
-               $this->database->cancelAtomic( __METHOD__ );
-               $this->database->endAtomic( __METHOD__ . '_outer' );
-               $this->assertNull( $callback1Called );
-               $this->assertNull( $callback2Called );
-               $this->assertEquals( IDatabase::TRIGGER_ROLLBACK, $callback3Called );
-
-               $this->database->startAtomic( __METHOD__ . '_outer' );
-               $this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
-               $this->database->startAtomic( __METHOD__ . '_inner' );
-               $this->database->onTransactionCommitOrIdle( $callback1, __METHOD__ );
-               $this->database->onTransactionPreCommitOrIdle( $callback2, __METHOD__ );
-               $this->database->onTransactionResolution( $callback3, __METHOD__ );
-               $this->database->cancelAtomic( __METHOD__ . '_inner' );
-               $this->database->cancelAtomic( __METHOD__ );
-               $this->database->endAtomic( __METHOD__ . '_outer' );
-               $this->assertNull( $callback1Called );
-               $this->assertNull( $callback2Called );
-               $this->assertEquals( IDatabase::TRIGGER_ROLLBACK, $callback3Called );
-
-               $wrapper = TestingAccessWrapper::newFromObject( $this->database );
-               $callback1Called = null;
-               $callback2Called = null;
-               $callback3Called = null;
-               $this->database->startAtomic( __METHOD__ . '_outer' );
-               $this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
-               $this->database->startAtomic( __METHOD__ . '_inner' );
-               $this->database->onTransactionCommitOrIdle( $callback1, __METHOD__ );
-               $this->database->onTransactionPreCommitOrIdle( $callback2, __METHOD__ );
-               $this->database->onTransactionResolution( $callback3, __METHOD__ );
-               $wrapper->trxStatus = Database::STATUS_TRX_ERROR;
-               $this->database->cancelAtomic( __METHOD__ . '_inner' );
-               $this->database->cancelAtomic( __METHOD__ );
-               $this->database->endAtomic( __METHOD__ . '_outer' );
-               $this->assertNull( $callback1Called );
-               $this->assertNull( $callback2Called );
-               $this->assertEquals( IDatabase::TRIGGER_ROLLBACK, $callback3Called );
-       }
-
-       /**
-        * @covers \Wikimedia\Rdbms\Database::doSavepoint
-        * @covers \Wikimedia\Rdbms\Database::doReleaseSavepoint
-        * @covers \Wikimedia\Rdbms\Database::doRollbackToSavepoint
-        * @covers \Wikimedia\Rdbms\Database::startAtomic
-        * @covers \Wikimedia\Rdbms\Database::endAtomic
-        * @covers \Wikimedia\Rdbms\Database::cancelAtomic
-        * @covers \Wikimedia\Rdbms\Database::doAtomicSection
-        */
-       public function testAtomicSectionsTrxRound() {
-               $this->database->setFlag( IDatabase::DBO_TRX );
-               $this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
-               $this->database->query( 'SELECT 1', __METHOD__ );
-               $this->database->endAtomic( __METHOD__ );
-               $this->database->commit( __METHOD__, IDatabase::FLUSHING_ALL_PEERS );
-               // phpcs:ignore Generic.Files.LineLength
-               $this->assertLastSql( 'BEGIN; SAVEPOINT wikimedia_rdbms_atomic1; SELECT 1; RELEASE SAVEPOINT wikimedia_rdbms_atomic1; COMMIT' );
-       }
-
-       public static function provideAtomicSectionMethodsForErrors() {
-               return [
-                       [ 'endAtomic' ],
-                       [ 'cancelAtomic' ],
-               ];
-       }
-
-       /**
-        * @dataProvider provideAtomicSectionMethodsForErrors
-        * @covers \Wikimedia\Rdbms\Database::endAtomic
-        * @covers \Wikimedia\Rdbms\Database::cancelAtomic
-        */
-       public function testNoAtomicSection( $method ) {
-               try {
-                       $this->database->$method( __METHOD__ );
-                       $this->fail( 'Expected exception not thrown' );
-               } catch ( DBUnexpectedError $ex ) {
-                       $this->assertSame(
-                               'No atomic section is open (got ' . __METHOD__ . ').',
-                               $ex->getMessage()
-                       );
-               }
-       }
-
-       /**
-        * @dataProvider provideAtomicSectionMethodsForErrors
-        * @covers \Wikimedia\Rdbms\Database::endAtomic
-        * @covers \Wikimedia\Rdbms\Database::cancelAtomic
-        */
-       public function testInvalidAtomicSectionEnded( $method ) {
-               $this->database->startAtomic( __METHOD__ . 'X' );
-               try {
-                       $this->database->$method( __METHOD__ );
-                       $this->fail( 'Expected exception not thrown' );
-               } catch ( DBUnexpectedError $ex ) {
-                       $this->assertSame(
-                               'Invalid atomic section ended (got ' . __METHOD__ . ' but expected ' .
-                                       __METHOD__ . 'X).',
-                               $ex->getMessage()
-                       );
-               }
-       }
-
-       /**
-        * @covers \Wikimedia\Rdbms\Database::cancelAtomic
-        */
-       public function testUncancellableAtomicSection() {
-               $this->database->startAtomic( __METHOD__ );
-               try {
-                       $this->database->cancelAtomic( __METHOD__ );
-                       $this->database->select( 'test', '1', [], __METHOD__ );
-                       $this->fail( 'Expected exception not thrown' );
-               } catch ( DBTransactionError $ex ) {
-                       $this->assertSame(
-                               'Cannot execute query from ' . __METHOD__ . ' while transaction status is ERROR.',
-                               $ex->getMessage()
-                       );
-               }
-       }
-
-       /**
-        * @expectedException \Wikimedia\Rdbms\DBTransactionStateError
-        * @covers \Wikimedia\Rdbms\Database::assertQueryIsCurrentlyAllowed
-        */
-       public function testTransactionErrorState1() {
-               $wrapper = TestingAccessWrapper::newFromObject( $this->database );
-
-               $this->database->begin( __METHOD__ );
-               $wrapper->trxStatus = Database::STATUS_TRX_ERROR;
-               $this->database->delete( 'x', [ 'field' => 3 ], __METHOD__ );
-               $this->database->commit( __METHOD__ );
-       }
-
-       /**
-        * @covers \Wikimedia\Rdbms\Database::query
-        */
-       public function testTransactionErrorState2() {
-               $wrapper = TestingAccessWrapper::newFromObject( $this->database );
-
-               $this->database->startAtomic( __METHOD__ );
-               $wrapper->trxStatus = Database::STATUS_TRX_ERROR;
-               $this->database->rollback( __METHOD__ );
-               $this->assertEquals( 0, $this->database->trxLevel() );
-               $this->assertEquals( Database::STATUS_TRX_NONE, $wrapper->trxStatus() );
-               $this->assertLastSql( 'BEGIN; ROLLBACK' );
-
-               $this->database->startAtomic( __METHOD__ );
-               $this->assertEquals( Database::STATUS_TRX_OK, $wrapper->trxStatus() );
-               $this->database->delete( 'x', [ 'field' => 1 ], __METHOD__ );
-               $this->database->endAtomic( __METHOD__ );
-               $this->assertEquals( Database::STATUS_TRX_NONE, $wrapper->trxStatus() );
-               $this->assertLastSql( 'BEGIN; DELETE FROM x WHERE field = \'1\'; COMMIT' );
-               $this->assertEquals( 0, $this->database->trxLevel(), 'Use after rollback()' );
-
-               $this->database->begin( __METHOD__ );
-               $this->database->startAtomic( __METHOD__, Database::ATOMIC_CANCELABLE );
-               $this->database->update( 'y', [ 'a' => 1 ], [ 'field' => 1 ], __METHOD__ );
-               $wrapper->trxStatus = Database::STATUS_TRX_ERROR;
-               $this->database->cancelAtomic( __METHOD__ );
-               $this->assertEquals( Database::STATUS_TRX_OK, $wrapper->trxStatus() );
-               $this->database->startAtomic( __METHOD__ );
-               $this->database->delete( 'y', [ 'field' => 1 ], __METHOD__ );
-               $this->database->endAtomic( __METHOD__ );
-               $this->database->commit( __METHOD__ );
-               // phpcs:ignore Generic.Files.LineLength
-               $this->assertLastSql( 'BEGIN; SAVEPOINT wikimedia_rdbms_atomic1; UPDATE y SET a = \'1\' WHERE field = \'1\'; ROLLBACK TO SAVEPOINT wikimedia_rdbms_atomic1; DELETE FROM y WHERE field = \'1\'; COMMIT' );
-               $this->assertEquals( 0, $this->database->trxLevel(), 'Use after rollback()' );
-
-               // Next transaction
-               $this->database->startAtomic( __METHOD__ );
-               $this->assertEquals( Database::STATUS_TRX_OK, $wrapper->trxStatus() );
-               $this->database->delete( 'x', [ 'field' => 3 ], __METHOD__ );
-               $this->database->endAtomic( __METHOD__ );
-               $this->assertEquals( Database::STATUS_TRX_NONE, $wrapper->trxStatus() );
-               $this->assertLastSql( 'BEGIN; DELETE FROM x WHERE field = \'3\'; COMMIT' );
-               $this->assertEquals( 0, $this->database->trxLevel() );
-       }
-
-       /**
-        * @covers \Wikimedia\Rdbms\Database::query
-        */
-       public function testImplicitTransactionRollback() {
-               $doError = function () {
-                       $this->database->forceNextQueryError( 666, 'Evilness' );
-                       try {
-                               $this->database->delete( 'error', '1', __CLASS__ . '::SomeCaller' );
-                               $this->fail( 'Expected exception not thrown' );
-                       } catch ( DBError $e ) {
-                               $this->assertSame( 666, $e->errno );
-                       }
-               };
-
-               $this->database->setFlag( Database::DBO_TRX );
-
-               // Implicit transaction does not get silently rolled back
-               $this->database->begin( __METHOD__, Database::TRANSACTION_INTERNAL );
-               call_user_func( $doError );
-               try {
-                       $this->database->delete( 'x', [ 'field' => 1 ], __METHOD__ );
-                       $this->fail( 'Expected exception not thrown' );
-               } catch ( DBTransactionError $e ) {
-                       $this->assertEquals(
-                               'Cannot execute query from ' . __METHOD__ . ' while transaction status is ERROR.',
-                               $e->getMessage()
-                       );
-               }
-               try {
-                       $this->database->commit( __METHOD__, Database::FLUSHING_INTERNAL );
-                       $this->fail( 'Expected exception not thrown' );
-               } catch ( DBTransactionError $e ) {
-                       $this->assertEquals(
-                               'Cannot execute query from ' . __METHOD__ . ' while transaction status is ERROR.',
-                               $e->getMessage()
-                       );
-               }
-               $this->database->rollback( __METHOD__, Database::FLUSHING_INTERNAL );
-               $this->assertLastSql( 'BEGIN; DELETE FROM error WHERE 1; ROLLBACK' );
-
-               // Likewise if there were prior writes
-               $this->database->begin( __METHOD__, Database::TRANSACTION_INTERNAL );
-               $this->database->delete( 'x', [ 'field' => 1 ], __METHOD__ );
-               call_user_func( $doError );
-               try {
-                       $this->database->delete( 'x', [ 'field' => 1 ], __METHOD__ );
-                       $this->fail( 'Expected exception not thrown' );
-               } catch ( DBTransactionStateError $e ) {
-               }
-               $this->database->rollback( __METHOD__, Database::FLUSHING_INTERNAL );
-               // phpcs:ignore
-               $this->assertLastSql( 'BEGIN; DELETE FROM x WHERE field = \'1\'; DELETE FROM error WHERE 1; ROLLBACK' );
-       }
-
-       /**
-        * @covers \Wikimedia\Rdbms\Database::query
-        */
-       public function testTransactionStatementRollbackIgnoring() {
-               $wrapper = TestingAccessWrapper::newFromObject( $this->database );
-               $warning = [];
-               $wrapper->deprecationLogger = function ( $msg ) use ( &$warning ) {
-                       $warning[] = $msg;
-               };
-
-               $doError = function () {
-                       $this->database->forceNextQueryError( 666, 'Evilness', [
-                               'wasKnownStatementRollbackError' => true,
-                       ] );
-                       try {
-                               $this->database->delete( 'error', '1', __CLASS__ . '::SomeCaller' );
-                               $this->fail( 'Expected exception not thrown' );
-                       } catch ( DBError $e ) {
-                               $this->assertSame( 666, $e->errno );
-                       }
-               };
-               $expectWarning = 'Caller from ' . __METHOD__ .
-                       ' ignored an error originally raised from ' . __CLASS__ . '::SomeCaller: [666] Evilness';
-
-               // Rollback doesn't raise a warning
-               $warning = [];
-               $this->database->startAtomic( __METHOD__ );
-               call_user_func( $doError );
-               $this->database->rollback( __METHOD__ );
-               $this->database->delete( 'x', [ 'field' => 1 ], __METHOD__ );
-               $this->assertSame( [], $warning );
-               // phpcs:ignore
-               $this->assertLastSql( 'BEGIN; DELETE FROM error WHERE 1; ROLLBACK; DELETE FROM x WHERE field = \'1\'' );
-
-               // cancelAtomic() doesn't raise a warning
-               $warning = [];
-               $this->database->begin( __METHOD__ );
-               $this->database->startAtomic( __METHOD__, Database::ATOMIC_CANCELABLE );
-               call_user_func( $doError );
-               $this->database->cancelAtomic( __METHOD__ );
-               $this->database->delete( 'x', [ 'field' => 1 ], __METHOD__ );
-               $this->database->commit( __METHOD__ );
-               $this->assertSame( [], $warning );
-               // phpcs:ignore
-               $this->assertLastSql( 'BEGIN; SAVEPOINT wikimedia_rdbms_atomic1; DELETE FROM error WHERE 1; ROLLBACK TO SAVEPOINT wikimedia_rdbms_atomic1; DELETE FROM x WHERE field = \'1\'; COMMIT' );
-
-               // Commit does raise a warning
-               $warning = [];
-               $this->database->begin( __METHOD__ );
-               call_user_func( $doError );
-               $this->database->commit( __METHOD__ );
-               $this->assertSame( [ $expectWarning ], $warning );
-               $this->assertLastSql( 'BEGIN; DELETE FROM error WHERE 1; COMMIT' );
-
-               // Deprecation only gets raised once
-               $warning = [];
-               $this->database->begin( __METHOD__ );
-               call_user_func( $doError );
-               $this->database->delete( 'x', [ 'field' => 1 ], __METHOD__ );
-               $this->database->commit( __METHOD__ );
-               $this->assertSame( [ $expectWarning ], $warning );
-               // phpcs:ignore
-               $this->assertLastSql( 'BEGIN; DELETE FROM error WHERE 1; DELETE FROM x WHERE field = \'1\'; COMMIT' );
-       }
-
-       /**
-        * @covers \Wikimedia\Rdbms\Database::close
-        */
-       public function testPrematureClose1() {
-               $fname = __METHOD__;
-               $this->database->begin( __METHOD__ );
-               $this->database->onTransactionCommitOrIdle( function () use ( $fname ) {
-                       $this->database->query( 'SELECT 1', $fname );
-               } );
-               $this->database->onTransactionResolution( function () use ( $fname ) {
-                       $this->database->query( 'SELECT 2', $fname );
-               } );
-               $this->database->delete( 'x', [ 'field' => 3 ], __METHOD__ );
-               try {
-                       $this->database->close();
-                       $this->fail( 'Expected exception not thrown' );
-               } catch ( DBUnexpectedError $ex ) {
-                       $this->assertSame(
-                               "Wikimedia\Rdbms\Database::close: transaction is still open (from $fname).",
-                               $ex->getMessage()
-                       );
-               }
-
-               $this->assertFalse( $this->database->isOpen() );
-               $this->assertLastSql( 'BEGIN; DELETE FROM x WHERE field = \'3\'; ROLLBACK; SELECT 2' );
-               $this->assertEquals( 0, $this->database->trxLevel() );
-       }
-
-       /**
-        * @covers \Wikimedia\Rdbms\Database::close
-        */
-       public function testPrematureClose2() {
-               try {
-                       $fname = __METHOD__;
-                       $this->database->startAtomic( __METHOD__ );
-                       $this->database->onTransactionCommitOrIdle( function () use ( $fname ) {
-                               $this->database->query( 'SELECT 1', $fname );
-                       } );
-                       $this->database->delete( 'x', [ 'field' => 3 ], __METHOD__ );
-                       $this->database->close();
-                       $this->fail( 'Expected exception not thrown' );
-               } catch ( DBUnexpectedError $ex ) {
-                       $this->assertSame(
-                               'Wikimedia\Rdbms\Database::close: atomic sections ' .
-                               'DatabaseSQLTest::testPrematureClose2 are still open.',
-                               $ex->getMessage()
-                       );
-               }
-
-               $this->assertFalse( $this->database->isOpen() );
-               $this->assertLastSql( 'BEGIN; DELETE FROM x WHERE field = \'3\'; ROLLBACK' );
-               $this->assertEquals( 0, $this->database->trxLevel() );
-       }
-
-       /**
-        * @covers \Wikimedia\Rdbms\Database::close
-        */
-       public function testPrematureClose3() {
-               try {
-                       $this->database->setFlag( IDatabase::DBO_TRX );
-                       $this->database->delete( 'x', [ 'field' => 3 ], __METHOD__ );
-                       $this->assertEquals( 1, $this->database->trxLevel() );
-                       $this->database->close();
-                       $this->fail( 'Expected exception not thrown' );
-               } catch ( DBUnexpectedError $ex ) {
-                       $this->assertSame(
-                               'Wikimedia\Rdbms\Database::close: ' .
-                               'mass commit/rollback of peer transaction required (DBO_TRX set).',
-                               $ex->getMessage()
-                       );
-               }
-
-               $this->assertFalse( $this->database->isOpen() );
-               $this->assertLastSql( 'BEGIN; DELETE FROM x WHERE field = \'3\'; ROLLBACK' );
-               $this->assertEquals( 0, $this->database->trxLevel() );
-       }
-
-       /**
-        * @covers \Wikimedia\Rdbms\Database::close
-        */
-       public function testPrematureClose4() {
-               $this->database->setFlag( IDatabase::DBO_TRX );
-               $this->database->query( 'SELECT 1', __METHOD__ );
-               $this->assertEquals( 1, $this->database->trxLevel() );
-               $this->database->close();
-               $this->database->clearFlag( IDatabase::DBO_TRX );
-
-               $this->assertFalse( $this->database->isOpen() );
-               $this->assertLastSql( 'BEGIN; SELECT 1; ROLLBACK' );
-               $this->assertEquals( 0, $this->database->trxLevel() );
-       }
-
-       /**
-        * @covers Wikimedia\Rdbms\Database::selectFieldValues()
-        */
-       public function testSelectFieldValues() {
-               $this->database->forceNextResult( [
-                       (object)[ 'value' => 'row1' ],
-                       (object)[ 'value' => 'row2' ],
-                       (object)[ 'value' => 'row3' ],
-               ] );
-
-               $this->assertSame(
-                       [ 'row1', 'row2', 'row3' ],
-                       $this->database->selectFieldValues( 'table', 'table.field', 'conds', __METHOD__ )
-               );
-               $this->assertLastSql( 'SELECT table.field AS value FROM table WHERE conds' );
-       }
-}
diff --git a/tests/phpunit/includes/libs/rdbms/database/DatabaseSqliteRdbmsTest.php b/tests/phpunit/includes/libs/rdbms/database/DatabaseSqliteRdbmsTest.php
deleted file mode 100644 (file)
index a886d6b..0000000
+++ /dev/null
@@ -1,60 +0,0 @@
-<?php
-
-use Wikimedia\Rdbms\DatabaseSqlite;
-
-/**
- * DatabaseSqliteTest is already defined in mediawiki core hence the 'Rdbms' included in this
- * class name.
- * The test in core should have mediawiki specific stuff removed and the tests moved to this
- * rdbms libs test.
- */
-class DatabaseSqliteRdbmsTest extends PHPUnit\Framework\TestCase {
-
-       use MediaWikiCoversValidator;
-       use PHPUnit4And6Compat;
-
-       /**
-        * @return PHPUnit_Framework_MockObject_MockObject|DatabaseSqlite
-        */
-       private function getMockDb() {
-               return $this->getMockBuilder( DatabaseSqlite::class )
-                       ->disableOriginalConstructor()
-                       ->setMethods( null )
-                       ->getMock();
-       }
-
-       public function provideBuildSubstring() {
-               yield [ 'someField', 1, 2, 'SUBSTR(someField,1,2)' ];
-               yield [ 'someField', 1, null, 'SUBSTR(someField,1)' ];
-       }
-
-       /**
-        * @covers Wikimedia\Rdbms\DatabaseSqlite::buildSubstring
-        * @dataProvider provideBuildSubstring
-        */
-       public function testBuildSubstring( $input, $start, $length, $expected ) {
-               $dbMock = $this->getMockDb();
-               $output = $dbMock->buildSubstring( $input, $start, $length );
-               $this->assertSame( $expected, $output );
-       }
-
-       public function provideBuildSubstring_invalidParams() {
-               yield [ -1, 1 ];
-               yield [ 1, -1 ];
-               yield [ 1, 'foo' ];
-               yield [ 'foo', 1 ];
-               yield [ null, 1 ];
-               yield [ 0, 1 ];
-       }
-
-       /**
-        * @covers Wikimedia\Rdbms\DatabaseSqlite::buildSubstring
-        * @dataProvider provideBuildSubstring_invalidParams
-        */
-       public function testBuildSubstring_invalidParams( $start, $length ) {
-               $dbMock = $this->getMockDb();
-               $this->setExpectedException( InvalidArgumentException::class );
-               $dbMock->buildSubstring( 'foo', $start, $length );
-       }
-
-}
diff --git a/tests/phpunit/includes/libs/rdbms/database/DatabaseTest.php b/tests/phpunit/includes/libs/rdbms/database/DatabaseTest.php
deleted file mode 100644 (file)
index 8b24791..0000000
+++ /dev/null
@@ -1,707 +0,0 @@
-<?php
-
-use Wikimedia\Rdbms\Database;
-use Wikimedia\Rdbms\IDatabase;
-use Wikimedia\Rdbms\DatabaseDomain;
-use Wikimedia\Rdbms\DatabaseMysqli;
-use Wikimedia\Rdbms\LBFactorySingle;
-use Wikimedia\Rdbms\TransactionProfiler;
-use Wikimedia\TestingAccessWrapper;
-use Wikimedia\Rdbms\DatabaseSqlite;
-use Wikimedia\Rdbms\DatabasePostgres;
-use Wikimedia\Rdbms\DatabaseMssql;
-use Wikimedia\Rdbms\DBUnexpectedError;
-
-class DatabaseTest extends PHPUnit\Framework\TestCase {
-       /** @var DatabaseTestHelper */
-       private $db;
-
-       use MediaWikiCoversValidator;
-
-       protected function setUp() {
-               $this->db = new DatabaseTestHelper( __CLASS__ . '::' . $this->getName() );
-       }
-
-       /**
-        * @dataProvider provideAddQuotes
-        * @covers Wikimedia\Rdbms\Database::factory
-        */
-       public function testFactory() {
-               $m = Database::NEW_UNCONNECTED; // no-connect mode
-               $p = [ 'host' => 'localhost', 'user' => 'me', 'password' => 'myself', 'dbname' => 'i' ];
-
-               $this->assertInstanceOf( DatabaseMysqli::class, Database::factory( 'mysqli', $p, $m ) );
-               $this->assertInstanceOf( DatabaseMysqli::class, Database::factory( 'MySqli', $p, $m ) );
-               $this->assertInstanceOf( DatabaseMysqli::class, Database::factory( 'MySQLi', $p, $m ) );
-               $this->assertInstanceOf( DatabasePostgres::class, Database::factory( 'postgres', $p, $m ) );
-               $this->assertInstanceOf( DatabasePostgres::class, Database::factory( 'Postgres', $p, $m ) );
-
-               $x = $p + [ 'port' => 10000, 'UseWindowsAuth' => false ];
-               $this->assertInstanceOf( DatabaseMssql::class, Database::factory( 'mssql', $x, $m ) );
-
-               $x = $p + [ 'dbFilePath' => 'some/file.sqlite' ];
-               $this->assertInstanceOf( DatabaseSqlite::class, Database::factory( 'sqlite', $x, $m ) );
-               $x = $p + [ 'dbDirectory' => 'some/file' ];
-               $this->assertInstanceOf( DatabaseSqlite::class, Database::factory( 'sqlite', $x, $m ) );
-       }
-
-       public static function provideAddQuotes() {
-               return [
-                       [ null, 'NULL' ],
-                       [ 1234, "'1234'" ],
-                       [ 1234.5678, "'1234.5678'" ],
-                       [ 'string', "'string'" ],
-                       [ 'string\'s cause trouble', "'string\'s cause trouble'" ],
-               ];
-       }
-
-       /**
-        * @dataProvider provideAddQuotes
-        * @covers Wikimedia\Rdbms\Database::addQuotes
-        */
-       public function testAddQuotes( $input, $expected ) {
-               $this->assertEquals( $expected, $this->db->addQuotes( $input ) );
-       }
-
-       public static function provideTableName() {
-               // Formatting is mostly ignored since addIdentifierQuotes is abstract.
-               // For testing of addIdentifierQuotes, see actual Database subclas tests.
-               return [
-                       'local' => [
-                               'tablename',
-                               'tablename',
-                               'quoted',
-                       ],
-                       'local-raw' => [
-                               'tablename',
-                               'tablename',
-                               'raw',
-                       ],
-                       'shared' => [
-                               'sharedb.tablename',
-                               'tablename',
-                               'quoted',
-                               [ 'dbname' => 'sharedb', 'schema' => null, 'prefix' => '' ],
-                       ],
-                       'shared-raw' => [
-                               'sharedb.tablename',
-                               'tablename',
-                               'raw',
-                               [ 'dbname' => 'sharedb', 'schema' => null, 'prefix' => '' ],
-                       ],
-                       'shared-prefix' => [
-                               'sharedb.sh_tablename',
-                               'tablename',
-                               'quoted',
-                               [ 'dbname' => 'sharedb', 'schema' => null, 'prefix' => 'sh_' ],
-                       ],
-                       'shared-prefix-raw' => [
-                               'sharedb.sh_tablename',
-                               'tablename',
-                               'raw',
-                               [ 'dbname' => 'sharedb', 'schema' => null, 'prefix' => 'sh_' ],
-                       ],
-                       'foreign' => [
-                               'databasename.tablename',
-                               'databasename.tablename',
-                               'quoted',
-                       ],
-                       'foreign-raw' => [
-                               'databasename.tablename',
-                               'databasename.tablename',
-                               'raw',
-                       ],
-               ];
-       }
-
-       /**
-        * @dataProvider provideTableName
-        * @covers Wikimedia\Rdbms\Database::tableName
-        */
-       public function testTableName( $expected, $table, $format, array $alias = null ) {
-               if ( $alias ) {
-                       $this->db->setTableAliases( [ $table => $alias ] );
-               }
-               $this->assertEquals(
-                       $expected,
-                       $this->db->tableName( $table, $format ?: 'quoted' )
-               );
-       }
-
-       public function provideTableNamesWithIndexClauseOrJOIN() {
-               return [
-                       'one-element array' => [
-                               [ 'table' ], [], 'table '
-                       ],
-                       'comma join' => [
-                               [ 'table1', 'table2' ], [], 'table1,table2 '
-                       ],
-                       'real join' => [
-                               [ 'table1', 'table2' ],
-                               [ 'table2' => [ 'LEFT JOIN', 't1_id = t2_id' ] ],
-                               'table1 LEFT JOIN table2 ON ((t1_id = t2_id))'
-                       ],
-                       'real join with multiple conditionals' => [
-                               [ 'table1', 'table2' ],
-                               [ 'table2' => [ 'LEFT JOIN', [ 't1_id = t2_id', 't2_x = \'X\'' ] ] ],
-                               'table1 LEFT JOIN table2 ON ((t1_id = t2_id) AND (t2_x = \'X\'))'
-                       ],
-                       'join with parenthesized group' => [
-                               [ 'table1', 'n' => [ 'table2', 'table3' ] ],
-                               [
-                                       'table3' => [ 'JOIN', 't2_id = t3_id' ],
-                                       'n' => [ 'LEFT JOIN', 't1_id = t2_id' ],
-                               ],
-                               'table1 LEFT JOIN (table2 JOIN table3 ON ((t2_id = t3_id))) ON ((t1_id = t2_id))'
-                       ],
-                       'join with degenerate parenthesized group' => [
-                               [ 'table1', 'n' => [ 't2' => 'table2' ] ],
-                               [
-                                       'n' => [ 'LEFT JOIN', 't1_id = t2_id' ],
-                               ],
-                               'table1 LEFT JOIN table2 t2 ON ((t1_id = t2_id))'
-                       ],
-               ];
-       }
-
-       /**
-        * @dataProvider provideTableNamesWithIndexClauseOrJOIN
-        * @covers Wikimedia\Rdbms\Database::tableNamesWithIndexClauseOrJOIN
-        */
-       public function testTableNamesWithIndexClauseOrJOIN( $tables, $join_conds, $expect ) {
-               $clause = TestingAccessWrapper::newFromObject( $this->db )
-                       ->tableNamesWithIndexClauseOrJOIN( $tables, [], [], $join_conds );
-               $this->assertSame( $expect, $clause );
-       }
-
-       /**
-        * @covers Wikimedia\Rdbms\Database::onTransactionCommitOrIdle
-        * @covers Wikimedia\Rdbms\Database::runOnTransactionIdleCallbacks
-        */
-       public function testTransactionIdle() {
-               $db = $this->db;
-
-               $db->clearFlag( DBO_TRX );
-               $called = false;
-               $flagSet = null;
-               $callback = function ( $trigger, IDatabase $db ) use ( &$flagSet, &$called ) {
-                       $called = true;
-                       $flagSet = $db->getFlag( DBO_TRX );
-               };
-
-               $db->onTransactionCommitOrIdle( $callback, __METHOD__ );
-               $this->assertTrue( $called, 'Callback reached' );
-               $this->assertFalse( $flagSet, 'DBO_TRX off in callback' );
-               $this->assertFalse( $db->getFlag( DBO_TRX ), 'DBO_TRX still default' );
-
-               $flagSet = null;
-               $called = false;
-               $db->startAtomic( __METHOD__ );
-               $db->onTransactionCommitOrIdle( $callback, __METHOD__ );
-               $this->assertFalse( $called, 'Callback not reached during TRX' );
-               $db->endAtomic( __METHOD__ );
-
-               $this->assertTrue( $called, 'Callback reached after COMMIT' );
-               $this->assertFalse( $flagSet, 'DBO_TRX off in callback' );
-               $this->assertFalse( $db->getFlag( DBO_TRX ), 'DBO_TRX restored to default' );
-
-               $db->clearFlag( DBO_TRX );
-               $db->onTransactionCommitOrIdle(
-                       function ( $trigger, IDatabase $db ) {
-                               $db->setFlag( DBO_TRX );
-                       },
-                       __METHOD__
-               );
-               $this->assertFalse( $db->getFlag( DBO_TRX ), 'DBO_TRX restored to default' );
-       }
-
-       /**
-        * @covers Wikimedia\Rdbms\Database::onTransactionCommitOrIdle
-        * @covers Wikimedia\Rdbms\Database::runOnTransactionIdleCallbacks
-        */
-       public function testTransactionIdle_TRX() {
-               $db = $this->getMockDB( [ 'isOpen', 'ping', 'getDBname' ] );
-               $db->method( 'isOpen' )->willReturn( true );
-               $db->method( 'ping' )->willReturn( true );
-               $db->method( 'getDBname' )->willReturn( '' );
-               $db->setFlag( DBO_TRX );
-
-               $lbFactory = LBFactorySingle::newFromConnection( $db );
-               // Ask for the connection so that LB sets internal state
-               // about this connection being the master connection
-               $lb = $lbFactory->getMainLB();
-               $conn = $lb->openConnection( $lb->getWriterIndex() );
-               $this->assertSame( $db, $conn, 'Same DB instance' );
-               $this->assertTrue( $db->getFlag( DBO_TRX ), 'DBO_TRX is set' );
-
-               $called = false;
-               $flagSet = null;
-               $callback = function () use ( $db, &$flagSet, &$called ) {
-                       $called = true;
-                       $flagSet = $db->getFlag( DBO_TRX );
-               };
-
-               $db->onTransactionCommitOrIdle( $callback, __METHOD__ );
-               $this->assertTrue( $called, 'Called when idle if DBO_TRX is set' );
-               $this->assertFalse( $flagSet, 'DBO_TRX off in callback' );
-               $this->assertTrue( $db->getFlag( DBO_TRX ), 'DBO_TRX still default' );
-
-               $called = false;
-               $lbFactory->beginMasterChanges( __METHOD__ );
-               $db->onTransactionCommitOrIdle( $callback, __METHOD__ );
-               $this->assertFalse( $called, 'Not called when lb-transaction is active' );
-
-               $lbFactory->commitMasterChanges( __METHOD__ );
-               $this->assertTrue( $called, 'Called when lb-transaction is committed' );
-
-               $called = false;
-               $lbFactory->beginMasterChanges( __METHOD__ );
-               $db->onTransactionCommitOrIdle( $callback, __METHOD__ );
-               $this->assertFalse( $called, 'Not called when lb-transaction is active' );
-
-               $lbFactory->rollbackMasterChanges( __METHOD__ );
-               $this->assertFalse( $called, 'Not called when lb-transaction is rolled back' );
-
-               $lbFactory->commitMasterChanges( __METHOD__ );
-               $this->assertFalse( $called, 'Not called in next round commit' );
-
-               $db->setFlag( DBO_TRX );
-               try {
-                       $db->onTransactionCommitOrIdle( function () {
-                               throw new RuntimeException( 'test' );
-                       } );
-                       $this->fail( "Exception not thrown" );
-               } catch ( RuntimeException $e ) {
-                       $this->assertTrue( $db->getFlag( DBO_TRX ) );
-               }
-       }
-
-       /**
-        * @covers Wikimedia\Rdbms\Database::onTransactionPreCommitOrIdle
-        * @covers Wikimedia\Rdbms\Database::runOnTransactionPreCommitCallbacks
-        */
-       public function testTransactionPreCommitOrIdle() {
-               $db = $this->getMockDB( [ 'isOpen' ] );
-               $db->method( 'isOpen' )->willReturn( true );
-               $db->clearFlag( DBO_TRX );
-
-               $this->assertFalse( $db->getFlag( DBO_TRX ), 'DBO_TRX is not set' );
-
-               $called = false;
-               $db->onTransactionPreCommitOrIdle(
-                       function ( IDatabase $db ) use ( &$called ) {
-                               $called = true;
-                       },
-                       __METHOD__
-               );
-               $this->assertTrue( $called, 'Called when idle' );
-
-               $db->begin( __METHOD__ );
-               $called = false;
-               $db->onTransactionPreCommitOrIdle(
-                       function ( IDatabase $db ) use ( &$called ) {
-                               $called = true;
-                       },
-                       __METHOD__
-               );
-               $this->assertFalse( $called, 'Not called when transaction is active' );
-               $db->commit( __METHOD__ );
-               $this->assertTrue( $called, 'Called when transaction is committed' );
-       }
-
-       /**
-        * @covers Wikimedia\Rdbms\Database::onTransactionPreCommitOrIdle
-        * @covers Wikimedia\Rdbms\Database::runOnTransactionPreCommitCallbacks
-        */
-       public function testTransactionPreCommitOrIdle_TRX() {
-               $db = $this->getMockDB( [ 'isOpen', 'ping', 'getDBname' ] );
-               $db->method( 'isOpen' )->willReturn( true );
-               $db->method( 'ping' )->willReturn( true );
-               $db->method( 'getDBname' )->willReturn( 'unittest' );
-               $db->setFlag( DBO_TRX );
-
-               $lbFactory = LBFactorySingle::newFromConnection( $db );
-               // Ask for the connection so that LB sets internal state
-               // about this connection being the master connection
-               $lb = $lbFactory->getMainLB();
-               $conn = $lb->openConnection( $lb->getWriterIndex() );
-               $this->assertSame( $db, $conn, 'Same DB instance' );
-
-               $this->assertFalse( $lb->hasMasterChanges() );
-               $this->assertTrue( $db->getFlag( DBO_TRX ), 'DBO_TRX is set' );
-               $called = false;
-               $callback = function ( IDatabase $db ) use ( &$called ) {
-                       $called = true;
-               };
-               $db->onTransactionPreCommitOrIdle( $callback, __METHOD__ );
-               $this->assertTrue( $called, 'Called when idle if DBO_TRX is set' );
-               $called = false;
-               $lbFactory->commitMasterChanges();
-               $this->assertFalse( $called );
-
-               $called = false;
-               $lbFactory->beginMasterChanges( __METHOD__ );
-               $db->onTransactionPreCommitOrIdle( $callback, __METHOD__ );
-               $this->assertFalse( $called, 'Not called when lb-transaction is active' );
-               $lbFactory->commitMasterChanges( __METHOD__ );
-               $this->assertTrue( $called, 'Called when lb-transaction is committed' );
-
-               $called = false;
-               $lbFactory->beginMasterChanges( __METHOD__ );
-               $db->onTransactionPreCommitOrIdle( $callback, __METHOD__ );
-               $this->assertFalse( $called, 'Not called when lb-transaction is active' );
-
-               $lbFactory->rollbackMasterChanges( __METHOD__ );
-               $this->assertFalse( $called, 'Not called when lb-transaction is rolled back' );
-
-               $lbFactory->commitMasterChanges( __METHOD__ );
-               $this->assertFalse( $called, 'Not called in next round commit' );
-       }
-
-       /**
-        * @covers Wikimedia\Rdbms\Database::onTransactionResolution
-        * @covers Wikimedia\Rdbms\Database::runOnTransactionIdleCallbacks
-        */
-       public function testTransactionResolution() {
-               $db = $this->db;
-
-               $db->clearFlag( DBO_TRX );
-               $db->begin( __METHOD__ );
-               $called = false;
-               $db->onTransactionResolution( function ( $trigger, IDatabase $db ) use ( &$called ) {
-                       $called = true;
-                       $db->setFlag( DBO_TRX );
-               } );
-               $db->commit( __METHOD__ );
-               $this->assertFalse( $db->getFlag( DBO_TRX ), 'DBO_TRX restored to default' );
-               $this->assertTrue( $called, 'Callback reached' );
-
-               $db->clearFlag( DBO_TRX );
-               $db->begin( __METHOD__ );
-               $called = false;
-               $db->onTransactionResolution( function ( $trigger, IDatabase $db ) use ( &$called ) {
-                       $called = true;
-                       $db->setFlag( DBO_TRX );
-               } );
-               $db->rollback( __METHOD__ );
-               $this->assertFalse( $db->getFlag( DBO_TRX ), 'DBO_TRX restored to default' );
-               $this->assertTrue( $called, 'Callback reached' );
-       }
-
-       /**
-        * @covers Wikimedia\Rdbms\Database::setTransactionListener
-        */
-       public function testTransactionListener() {
-               $db = $this->db;
-
-               $db->setTransactionListener( 'ping', function () use ( $db, &$called ) {
-                       $called = true;
-               } );
-
-               $called = false;
-               $db->begin( __METHOD__ );
-               $db->commit( __METHOD__ );
-               $this->assertTrue( $called, 'Callback reached' );
-
-               $called = false;
-               $db->begin( __METHOD__ );
-               $db->commit( __METHOD__ );
-               $this->assertTrue( $called, 'Callback still reached' );
-
-               $called = false;
-               $db->begin( __METHOD__ );
-               $db->rollback( __METHOD__ );
-               $this->assertTrue( $called, 'Callback reached' );
-
-               $db->setTransactionListener( 'ping', null );
-               $called = false;
-               $db->begin( __METHOD__ );
-               $db->commit( __METHOD__ );
-               $this->assertFalse( $called, 'Callback not reached' );
-       }
-
-       /**
-        * Use this mock instead of DatabaseTestHelper for cases where
-        * DatabaseTestHelper is too inflexibile due to mocking too much
-        * or being too restrictive about fname matching (e.g. for tests
-        * that assert behaviour when the name is a mismatch, we need to
-        * catch the error here instead of there).
-        *
-        * @return Database
-        */
-       private function getMockDB( $methods = [] ) {
-               static $abstractMethods = [
-                       'fetchAffectedRowCount',
-                       'closeConnection',
-                       'dataSeek',
-                       'doQuery',
-                       'fetchObject', 'fetchRow',
-                       'fieldInfo', 'fieldName',
-                       'getSoftwareLink', 'getServerVersion',
-                       'getType',
-                       'indexInfo',
-                       'insertId',
-                       'lastError', 'lastErrno',
-                       'numFields', 'numRows',
-                       'open',
-                       'strencode',
-                       'tableExists'
-               ];
-               $db = $this->getMockBuilder( Database::class )
-                       ->disableOriginalConstructor()
-                       ->setMethods( array_values( array_unique( array_merge(
-                               $abstractMethods,
-                               $methods
-                       ) ) ) )
-                       ->getMock();
-               $wdb = TestingAccessWrapper::newFromObject( $db );
-               $wdb->trxProfiler = new TransactionProfiler();
-               $wdb->connLogger = new \Psr\Log\NullLogger();
-               $wdb->queryLogger = new \Psr\Log\NullLogger();
-               $wdb->currentDomain = DatabaseDomain::newUnspecified();
-               return $db;
-       }
-
-       /**
-        * @covers Wikimedia\Rdbms\Database::flushSnapshot
-        */
-       public function testFlushSnapshot() {
-               $db = $this->getMockDB( [ 'isOpen' ] );
-               $db->method( 'isOpen' )->willReturn( true );
-
-               $db->flushSnapshot( __METHOD__ ); // ok
-               $db->flushSnapshot( __METHOD__ ); // ok
-
-               $db->setFlag( DBO_TRX, $db::REMEMBER_PRIOR );
-               $db->query( 'SELECT 1', __METHOD__ );
-               $this->assertTrue( (bool)$db->trxLevel(), "Transaction started." );
-               $db->flushSnapshot( __METHOD__ ); // ok
-               $db->restoreFlags( $db::RESTORE_PRIOR );
-
-               $this->assertFalse( (bool)$db->trxLevel(), "Transaction cleared." );
-       }
-
-       /**
-        * @covers Wikimedia\Rdbms\Database::getScopedLockAndFlush
-        * @covers Wikimedia\Rdbms\Database::lock
-        * @covers Wikimedia\Rdbms\Database::unlock
-        * @covers Wikimedia\Rdbms\Database::lockIsFree
-        */
-       public function testGetScopedLock() {
-               $db = $this->getMockDB( [ 'isOpen', 'getDBname' ] );
-               $db->method( 'isOpen' )->willReturn( true );
-               $db->method( 'getDBname' )->willReturn( 'unittest' );
-
-               $this->assertEquals( 0, $db->trxLevel() );
-               $this->assertEquals( true, $db->lockIsFree( 'x', __METHOD__ ) );
-               $this->assertEquals( true, $db->lock( 'x', __METHOD__ ) );
-               $this->assertEquals( false, $db->lockIsFree( 'x', __METHOD__ ) );
-               $this->assertEquals( true, $db->unlock( 'x', __METHOD__ ) );
-               $this->assertEquals( true, $db->lockIsFree( 'x', __METHOD__ ) );
-               $this->assertEquals( 0, $db->trxLevel() );
-
-               $db->setFlag( DBO_TRX );
-               $this->assertEquals( true, $db->lockIsFree( 'x', __METHOD__ ) );
-               $this->assertEquals( true, $db->lock( 'x', __METHOD__ ) );
-               $this->assertEquals( false, $db->lockIsFree( 'x', __METHOD__ ) );
-               $this->assertEquals( true, $db->unlock( 'x', __METHOD__ ) );
-               $this->assertEquals( true, $db->lockIsFree( 'x', __METHOD__ ) );
-               $db->clearFlag( DBO_TRX );
-
-               // Pending writes with DBO_TRX
-               $this->assertEquals( 0, $db->trxLevel() );
-               $this->assertTrue( $db->lockIsFree( 'meow', __METHOD__ ) );
-               $db->setFlag( DBO_TRX );
-               $db->query( "DELETE FROM test WHERE t = 1" ); // trigger DBO_TRX transaction before lock
-               try {
-                       $lock = $db->getScopedLockAndFlush( 'meow', __METHOD__, 1 );
-                       $this->fail( "Exception not reached" );
-               } catch ( DBUnexpectedError $e ) {
-                       $this->assertEquals( 1, $db->trxLevel(), "Transaction not committed." );
-                       $this->assertTrue( $db->lockIsFree( 'meow', __METHOD__ ), 'Lock not acquired' );
-               }
-               $db->rollback( __METHOD__, IDatabase::FLUSHING_ALL_PEERS );
-               // Pending writes without DBO_TRX
-               $db->clearFlag( DBO_TRX );
-               $this->assertEquals( 0, $db->trxLevel() );
-               $this->assertTrue( $db->lockIsFree( 'meow2', __METHOD__ ) );
-               $db->begin( __METHOD__ );
-               $db->query( "DELETE FROM test WHERE t = 1" ); // trigger DBO_TRX transaction before lock
-               try {
-                       $lock = $db->getScopedLockAndFlush( 'meow2', __METHOD__, 1 );
-                       $this->fail( "Exception not reached" );
-               } catch ( DBUnexpectedError $e ) {
-                       $this->assertEquals( 1, $db->trxLevel(), "Transaction not committed." );
-                       $this->assertTrue( $db->lockIsFree( 'meow2', __METHOD__ ), 'Lock not acquired' );
-               }
-               $db->rollback( __METHOD__ );
-               // No pending writes, with DBO_TRX
-               $db->setFlag( DBO_TRX );
-               $this->assertEquals( 0, $db->trxLevel() );
-               $this->assertTrue( $db->lockIsFree( 'wuff', __METHOD__ ) );
-               $db->query( "SELECT 1", __METHOD__ );
-               $this->assertEquals( 1, $db->trxLevel() );
-               $lock = $db->getScopedLockAndFlush( 'wuff', __METHOD__, 1 );
-               $this->assertEquals( 0, $db->trxLevel() );
-               $this->assertFalse( $db->lockIsFree( 'wuff', __METHOD__ ), 'Lock already acquired' );
-               $db->rollback( __METHOD__, IDatabase::FLUSHING_ALL_PEERS );
-               // No pending writes, without DBO_TRX
-               $db->clearFlag( DBO_TRX );
-               $this->assertEquals( 0, $db->trxLevel() );
-               $this->assertTrue( $db->lockIsFree( 'wuff2', __METHOD__ ) );
-               $db->begin( __METHOD__ );
-               try {
-                       $lock = $db->getScopedLockAndFlush( 'wuff2', __METHOD__, 1 );
-                       $this->fail( "Exception not reached" );
-               } catch ( DBUnexpectedError $e ) {
-                       $this->assertEquals( 1, $db->trxLevel(), "Transaction not committed." );
-                       $this->assertFalse( $db->lockIsFree( 'wuff2', __METHOD__ ), 'Lock not acquired' );
-               }
-               $db->rollback( __METHOD__ );
-       }
-
-       /**
-        * @covers Wikimedia\Rdbms\Database::getFlag
-        * @covers Wikimedia\Rdbms\Database::setFlag
-        * @covers Wikimedia\Rdbms\Database::restoreFlags
-        */
-       public function testFlagSetting() {
-               $db = $this->db;
-               $origTrx = $db->getFlag( DBO_TRX );
-               $origSsl = $db->getFlag( DBO_SSL );
-
-               $origTrx
-                       ? $db->clearFlag( DBO_TRX, $db::REMEMBER_PRIOR )
-                       : $db->setFlag( DBO_TRX, $db::REMEMBER_PRIOR );
-               $this->assertEquals( !$origTrx, $db->getFlag( DBO_TRX ) );
-
-               $origSsl
-                       ? $db->clearFlag( DBO_SSL, $db::REMEMBER_PRIOR )
-                       : $db->setFlag( DBO_SSL, $db::REMEMBER_PRIOR );
-               $this->assertEquals( !$origSsl, $db->getFlag( DBO_SSL ) );
-
-               $db->restoreFlags( $db::RESTORE_INITIAL );
-               $this->assertEquals( $origTrx, $db->getFlag( DBO_TRX ) );
-               $this->assertEquals( $origSsl, $db->getFlag( DBO_SSL ) );
-
-               $origTrx
-                       ? $db->clearFlag( DBO_TRX, $db::REMEMBER_PRIOR )
-                       : $db->setFlag( DBO_TRX, $db::REMEMBER_PRIOR );
-               $origSsl
-                       ? $db->clearFlag( DBO_SSL, $db::REMEMBER_PRIOR )
-                       : $db->setFlag( DBO_SSL, $db::REMEMBER_PRIOR );
-
-               $db->restoreFlags();
-               $this->assertEquals( $origSsl, $db->getFlag( DBO_SSL ) );
-               $this->assertEquals( !$origTrx, $db->getFlag( DBO_TRX ) );
-
-               $db->restoreFlags();
-               $this->assertEquals( $origSsl, $db->getFlag( DBO_SSL ) );
-               $this->assertEquals( $origTrx, $db->getFlag( DBO_TRX ) );
-       }
-
-       /**
-        * @expectedException UnexpectedValueException
-        * @covers Wikimedia\Rdbms\Database::setFlag
-        */
-       public function testDBOIgnoreSet() {
-               $db = $this->getMockBuilder( DatabaseMysqli::class )
-                       ->disableOriginalConstructor()
-                       ->setMethods( null )
-                       ->getMock();
-
-               $db->setFlag( Database::DBO_IGNORE );
-       }
-
-       /**
-        * @expectedException UnexpectedValueException
-        * @covers Wikimedia\Rdbms\Database::clearFlag
-        */
-       public function testDBOIgnoreClear() {
-               $db = $this->getMockBuilder( DatabaseMysqli::class )
-                       ->disableOriginalConstructor()
-                       ->setMethods( null )
-                       ->getMock();
-
-               $db->clearFlag( Database::DBO_IGNORE );
-       }
-
-       /**
-        * @covers Wikimedia\Rdbms\Database::tablePrefix
-        * @covers Wikimedia\Rdbms\Database::dbSchema
-        */
-       public function testSchemaAndPrefixMutators() {
-               $ud = DatabaseDomain::newUnspecified();
-
-               $this->assertEquals( $ud->getId(), $this->db->getDomainID() );
-
-               $old = $this->db->tablePrefix();
-               $oldDomain = $this->db->getDomainId();
-               $this->assertInternalType( 'string', $old, 'Prefix is string' );
-               $this->assertSame( $old, $this->db->tablePrefix(), "Prefix unchanged" );
-               $this->assertSame( $old, $this->db->tablePrefix( 'xxx_' ) );
-               $this->assertSame( 'xxx_', $this->db->tablePrefix(), "Prefix set" );
-               $this->db->tablePrefix( $old );
-               $this->assertNotEquals( 'xxx_', $this->db->tablePrefix() );
-               $this->assertSame( $oldDomain, $this->db->getDomainId() );
-
-               $old = $this->db->dbSchema();
-               $oldDomain = $this->db->getDomainId();
-               $this->assertInternalType( 'string', $old, 'Schema is string' );
-               $this->assertSame( $old, $this->db->dbSchema(), "Schema unchanged" );
-
-               $this->db->selectDB( 'y' );
-               $this->assertSame( $old, $this->db->dbSchema( 'xxx' ) );
-               $this->assertSame( 'xxx', $this->db->dbSchema(), "Schema set" );
-               $this->db->dbSchema( $old );
-               $this->assertNotEquals( 'xxx', $this->db->dbSchema() );
-               $this->assertSame( "y", $this->db->getDomainId() );
-       }
-
-       /**
-        * @covers Wikimedia\Rdbms\Database::tablePrefix
-        * @covers Wikimedia\Rdbms\Database::dbSchema
-        * @expectedException DBUnexpectedError
-        */
-       public function testSchemaWithNoDB() {
-               $ud = DatabaseDomain::newUnspecified();
-
-               $this->assertEquals( $ud->getId(), $this->db->getDomainID() );
-               $this->assertSame( '', $this->db->dbSchema() );
-
-               $this->db->dbSchema( 'xxx' );
-       }
-
-       /**
-        * @covers Wikimedia\Rdbms\Database::selectDomain
-        */
-       public function testSelectDomain() {
-               $oldDomain = $this->db->getDomainId();
-               $oldDatabase = $this->db->getDBname();
-               $oldSchema = $this->db->dbSchema();
-               $oldPrefix = $this->db->tablePrefix();
-
-               $this->db->selectDomain( 'testselectdb-xxx_' );
-               $this->assertSame( 'testselectdb', $this->db->getDBname() );
-               $this->assertSame( '', $this->db->dbSchema() );
-               $this->assertSame( 'xxx_', $this->db->tablePrefix() );
-
-               $this->db->selectDomain( $oldDomain );
-               $this->assertSame( $oldDatabase, $this->db->getDBname() );
-               $this->assertSame( $oldSchema, $this->db->dbSchema() );
-               $this->assertSame( $oldPrefix, $this->db->tablePrefix() );
-               $this->assertSame( $oldDomain, $this->db->getDomainId() );
-
-               $this->db->selectDomain( 'testselectdb-schema-xxx_' );
-               $this->assertSame( 'testselectdb', $this->db->getDBname() );
-               $this->assertSame( 'schema', $this->db->dbSchema() );
-               $this->assertSame( 'xxx_', $this->db->tablePrefix() );
-
-               $this->db->selectDomain( $oldDomain );
-               $this->assertSame( $oldDatabase, $this->db->getDBname() );
-               $this->assertSame( $oldSchema, $this->db->dbSchema() );
-               $this->assertSame( $oldPrefix, $this->db->tablePrefix() );
-               $this->assertSame( $oldDomain, $this->db->getDomainId() );
-       }
-
-}
diff --git a/tests/phpunit/includes/libs/services/ServiceContainerTest.php b/tests/phpunit/includes/libs/services/ServiceContainerTest.php
deleted file mode 100644 (file)
index 6e51883..0000000
+++ /dev/null
@@ -1,497 +0,0 @@
-<?php
-
-use Wikimedia\Services\ServiceContainer;
-
-/**
- * @covers Wikimedia\Services\ServiceContainer
- */
-class ServiceContainerTest extends PHPUnit\Framework\TestCase {
-
-       use MediaWikiCoversValidator; // TODO this library is supposed to be independent of MediaWiki
-       use PHPUnit4And6Compat;
-
-       private function newServiceContainer( $extraArgs = [] ) {
-               return new ServiceContainer( $extraArgs );
-       }
-
-       public function testGetServiceNames() {
-               $services = $this->newServiceContainer();
-               $names = $services->getServiceNames();
-
-               $this->assertInternalType( 'array', $names );
-               $this->assertEmpty( $names );
-
-               $name = 'TestService92834576';
-               $services->defineService( $name, function () {
-                       return null;
-               } );
-
-               $names = $services->getServiceNames();
-               $this->assertContains( $name, $names );
-       }
-
-       public function testHasService() {
-               $services = $this->newServiceContainer();
-
-               $name = 'TestService92834576';
-               $this->assertFalse( $services->hasService( $name ) );
-
-               $services->defineService( $name, function () {
-                       return null;
-               } );
-
-               $this->assertTrue( $services->hasService( $name ) );
-       }
-
-       public function testGetService() {
-               $services = $this->newServiceContainer( [ 'Foo' ] );
-
-               $theService = new stdClass();
-               $name = 'TestService92834576';
-               $count = 0;
-
-               $services->defineService(
-                       $name,
-                       function ( $actualLocator, $extra ) use ( $services, $theService, &$count ) {
-                               $count++;
-                               PHPUnit_Framework_Assert::assertSame( $services, $actualLocator );
-                               PHPUnit_Framework_Assert::assertSame( $extra, 'Foo' );
-                               return $theService;
-                       }
-               );
-
-               $this->assertSame( $theService, $services->getService( $name ) );
-
-               $services->getService( $name );
-               $this->assertSame( 1, $count, 'instantiator should be called exactly once!' );
-       }
-
-       public function testGetService_fail_unknown() {
-               $services = $this->newServiceContainer();
-
-               $name = 'TestService92834576';
-
-               $this->setExpectedException( Wikimedia\Services\NoSuchServiceException::class );
-
-               $services->getService( $name );
-       }
-
-       public function testPeekService() {
-               $services = $this->newServiceContainer();
-
-               $services->defineService(
-                       'Foo',
-                       function () {
-                               return new stdClass();
-                       }
-               );
-
-               $services->defineService(
-                       'Bar',
-                       function () {
-                               return new stdClass();
-                       }
-               );
-
-               // trigger instantiation of Foo
-               $services->getService( 'Foo' );
-
-               $this->assertInternalType(
-                       'object',
-                       $services->peekService( 'Foo' ),
-                       'Peek should return the service object if it had been accessed before.'
-               );
-
-               $this->assertNull(
-                       $services->peekService( 'Bar' ),
-                       'Peek should return null if the service was never accessed.'
-               );
-       }
-
-       public function testPeekService_fail_unknown() {
-               $services = $this->newServiceContainer();
-
-               $name = 'TestService92834576';
-
-               $this->setExpectedException( Wikimedia\Services\NoSuchServiceException::class );
-
-               $services->peekService( $name );
-       }
-
-       public function testDefineService() {
-               $services = $this->newServiceContainer();
-
-               $theService = new stdClass();
-               $name = 'TestService92834576';
-
-               $services->defineService( $name, function ( $actualLocator ) use ( $services, $theService ) {
-                       PHPUnit_Framework_Assert::assertSame( $services, $actualLocator );
-                       return $theService;
-               } );
-
-               $this->assertTrue( $services->hasService( $name ) );
-               $this->assertSame( $theService, $services->getService( $name ) );
-       }
-
-       public function testDefineService_fail_duplicate() {
-               $services = $this->newServiceContainer();
-
-               $theService = new stdClass();
-               $name = 'TestService92834576';
-
-               $services->defineService( $name, function () use ( $theService ) {
-                       return $theService;
-               } );
-
-               $this->setExpectedException( Wikimedia\Services\ServiceAlreadyDefinedException::class );
-
-               $services->defineService( $name, function () use ( $theService ) {
-                       return $theService;
-               } );
-       }
-
-       public function testApplyWiring() {
-               $services = $this->newServiceContainer();
-
-               $wiring = [
-                       'Foo' => function () {
-                               return 'Foo!';
-                       },
-                       'Bar' => function () {
-                               return 'Bar!';
-                       },
-               ];
-
-               $services->applyWiring( $wiring );
-
-               $this->assertSame( 'Foo!', $services->getService( 'Foo' ) );
-               $this->assertSame( 'Bar!', $services->getService( 'Bar' ) );
-       }
-
-       public function testImportWiring() {
-               $services = $this->newServiceContainer();
-
-               $wiring = [
-                       'Foo' => function () {
-                               return 'Foo!';
-                       },
-                       'Bar' => function () {
-                               return 'Bar!';
-                       },
-                       'Car' => function () {
-                               return 'FUBAR!';
-                       },
-               ];
-
-               $services->applyWiring( $wiring );
-
-               $services->addServiceManipulator( 'Foo', function ( $service ) {
-                       return $service . '+X';
-               } );
-
-               $services->addServiceManipulator( 'Car', function ( $service ) {
-                       return $service . '+X';
-               } );
-
-               $newServices = $this->newServiceContainer();
-
-               // create a service with manipulator
-               $newServices->defineService( 'Foo', function () {
-                       return 'Foo!';
-               } );
-
-               $newServices->addServiceManipulator( 'Foo', function ( $service ) {
-                       return $service . '+Y';
-               } );
-
-               // create a service before importing, so we can later check that
-               // existing service instances survive importWiring()
-               $newServices->defineService( 'Car', function () {
-                       return 'Car!';
-               } );
-
-               // force instantiation
-               $newServices->getService( 'Car' );
-
-               // Define another service, so we can later check that extra wiring
-               // is not lost.
-               $newServices->defineService( 'Xar', function () {
-                       return 'Xar!';
-               } );
-
-               // import wiring, but skip `Bar`
-               $newServices->importWiring( $services, [ 'Bar' ] );
-
-               $this->assertNotContains( 'Bar', $newServices->getServiceNames(), 'Skip `Bar` service' );
-               $this->assertSame( 'Foo!+Y+X', $newServices->getService( 'Foo' ) );
-
-               // import all wiring, but preserve existing service instance
-               $newServices->importWiring( $services );
-
-               $this->assertContains( 'Bar', $newServices->getServiceNames(), 'Import all services' );
-               $this->assertSame( 'Bar!', $newServices->getService( 'Bar' ) );
-               $this->assertSame( 'Car!', $newServices->getService( 'Car' ), 'Use existing service instance' );
-               $this->assertSame( 'Xar!', $newServices->getService( 'Xar' ), 'Predefined services are kept' );
-       }
-
-       public function testLoadWiringFiles() {
-               $services = $this->newServiceContainer();
-
-               $wiringFiles = [
-                       __DIR__ . '/TestWiring1.php',
-                       __DIR__ . '/TestWiring2.php',
-               ];
-
-               $services->loadWiringFiles( $wiringFiles );
-
-               $this->assertSame( 'Foo!', $services->getService( 'Foo' ) );
-               $this->assertSame( 'Bar!', $services->getService( 'Bar' ) );
-       }
-
-       public function testLoadWiringFiles_fail_duplicate() {
-               $services = $this->newServiceContainer();
-
-               $wiringFiles = [
-                       __DIR__ . '/TestWiring1.php',
-                       __DIR__ . '/./TestWiring1.php',
-               ];
-
-               // loading the same file twice should fail, because
-               $this->setExpectedException( Wikimedia\Services\ServiceAlreadyDefinedException::class );
-
-               $services->loadWiringFiles( $wiringFiles );
-       }
-
-       public function testRedefineService() {
-               $services = $this->newServiceContainer( [ 'Foo' ] );
-
-               $theService1 = new stdClass();
-               $name = 'TestService92834576';
-
-               $services->defineService( $name, function () {
-                       PHPUnit_Framework_Assert::fail(
-                               'The original instantiator function should not get called'
-                       );
-               } );
-
-               // redefine before instantiation
-               $services->redefineService(
-                       $name,
-                       function ( $actualLocator, $extra ) use ( $services, $theService1 ) {
-                               PHPUnit_Framework_Assert::assertSame( $services, $actualLocator );
-                               PHPUnit_Framework_Assert::assertSame( 'Foo', $extra );
-                               return $theService1;
-                       }
-               );
-
-               // force instantiation, check result
-               $this->assertSame( $theService1, $services->getService( $name ) );
-       }
-
-       public function testRedefineService_disabled() {
-               $services = $this->newServiceContainer( [ 'Foo' ] );
-
-               $theService1 = new stdClass();
-               $name = 'TestService92834576';
-
-               $services->defineService( $name, function () {
-                       return 'Foo';
-               } );
-
-               // disable the service. we should be able to redefine it anyway.
-               $services->disableService( $name );
-
-               $services->redefineService( $name, function () use ( $theService1 ) {
-                       return $theService1;
-               } );
-
-               // force instantiation, check result
-               $this->assertSame( $theService1, $services->getService( $name ) );
-       }
-
-       public function testRedefineService_fail_undefined() {
-               $services = $this->newServiceContainer();
-
-               $theService = new stdClass();
-               $name = 'TestService92834576';
-
-               $this->setExpectedException( Wikimedia\Services\NoSuchServiceException::class );
-
-               $services->redefineService( $name, function () use ( $theService ) {
-                       return $theService;
-               } );
-       }
-
-       public function testRedefineService_fail_in_use() {
-               $services = $this->newServiceContainer( [ 'Foo' ] );
-
-               $theService = new stdClass();
-               $name = 'TestService92834576';
-
-               $services->defineService( $name, function () {
-                       return 'Foo';
-               } );
-
-               // create the service, so it can no longer be redefined
-               $services->getService( $name );
-
-               $this->setExpectedException( Wikimedia\Services\CannotReplaceActiveServiceException::class );
-
-               $services->redefineService( $name, function () use ( $theService ) {
-                       return $theService;
-               } );
-       }
-
-       public function testAddServiceManipulator() {
-               $services = $this->newServiceContainer( [ 'Foo' ] );
-
-               $theService1 = new stdClass();
-               $theService2 = new stdClass();
-               $name = 'TestService92834576';
-
-               $services->defineService(
-                       $name,
-                       function ( $actualLocator, $extra ) use ( $services, $theService1 ) {
-                               PHPUnit_Framework_Assert::assertSame( $services, $actualLocator );
-                               PHPUnit_Framework_Assert::assertSame( 'Foo', $extra );
-                               return $theService1;
-                       }
-               );
-
-               $services->addServiceManipulator(
-                       $name,
-                       function (
-                               $theService, $actualLocator, $extra
-                       ) use (
-                               $services, $theService1, $theService2
-                       ) {
-                               PHPUnit_Framework_Assert::assertSame( $theService1, $theService );
-                               PHPUnit_Framework_Assert::assertSame( $services, $actualLocator );
-                               PHPUnit_Framework_Assert::assertSame( 'Foo', $extra );
-                               return $theService2;
-                       }
-               );
-
-               // force instantiation, check result
-               $this->assertSame( $theService2, $services->getService( $name ) );
-       }
-
-       public function testAddServiceManipulator_fail_undefined() {
-               $services = $this->newServiceContainer();
-
-               $theService = new stdClass();
-               $name = 'TestService92834576';
-
-               $this->setExpectedException( Wikimedia\Services\NoSuchServiceException::class );
-
-               $services->addServiceManipulator( $name, function () use ( $theService ) {
-                       return $theService;
-               } );
-       }
-
-       public function testAddServiceManipulator_fail_in_use() {
-               $services = $this->newServiceContainer( [ 'Foo' ] );
-
-               $theService = new stdClass();
-               $name = 'TestService92834576';
-
-               $services->defineService( $name, function () use ( $theService ) {
-                       return $theService;
-               } );
-
-               // create the service, so it can no longer be redefined
-               $services->getService( $name );
-
-               $this->setExpectedException( Wikimedia\Services\CannotReplaceActiveServiceException::class );
-
-               $services->addServiceManipulator( $name, function () {
-                       return 'Foo';
-               } );
-       }
-
-       public function testDisableService() {
-               $services = $this->newServiceContainer( [ 'Foo' ] );
-
-               $destructible = $this->getMockBuilder( Wikimedia\Services\DestructibleService::class )
-                       ->getMock();
-               $destructible->expects( $this->once() )
-                       ->method( 'destroy' );
-
-               $services->defineService( 'Foo', function () use ( $destructible ) {
-                       return $destructible;
-               } );
-               $services->defineService( 'Bar', function () {
-                       return new stdClass();
-               } );
-               $services->defineService( 'Qux', function () {
-                       return new stdClass();
-               } );
-
-               // instantiate Foo and Bar services
-               $services->getService( 'Foo' );
-               $services->getService( 'Bar' );
-
-               // disable service, should call destroy() once.
-               $services->disableService( 'Foo' );
-
-               // disabled service should still be listed
-               $this->assertContains( 'Foo', $services->getServiceNames() );
-
-               // getting other services should still work
-               $services->getService( 'Bar' );
-
-               // disable non-destructible service, and not-yet-instantiated service
-               $services->disableService( 'Bar' );
-               $services->disableService( 'Qux' );
-
-               $this->assertNull( $services->peekService( 'Bar' ) );
-               $this->assertNull( $services->peekService( 'Qux' ) );
-
-               // disabled service should still be listed
-               $this->assertContains( 'Bar', $services->getServiceNames() );
-               $this->assertContains( 'Qux', $services->getServiceNames() );
-
-               $this->setExpectedException( Wikimedia\Services\ServiceDisabledException::class );
-               $services->getService( 'Qux' );
-       }
-
-       public function testDisableService_fail_undefined() {
-               $services = $this->newServiceContainer();
-
-               $theService = new stdClass();
-               $name = 'TestService92834576';
-
-               $this->setExpectedException( Wikimedia\Services\NoSuchServiceException::class );
-
-               $services->redefineService( $name, function () use ( $theService ) {
-                       return $theService;
-               } );
-       }
-
-       public function testDestroy() {
-               $services = $this->newServiceContainer();
-
-               $destructible = $this->getMockBuilder( Wikimedia\Services\DestructibleService::class )
-                       ->getMock();
-               $destructible->expects( $this->once() )
-                       ->method( 'destroy' );
-
-               $services->defineService( 'Foo', function () use ( $destructible ) {
-                       return $destructible;
-               } );
-
-               $services->defineService( 'Bar', function () {
-                       return new stdClass();
-               } );
-
-               // create the service
-               $services->getService( 'Foo' );
-
-               // destroy the container
-               $services->destroy();
-
-               $this->setExpectedException( Wikimedia\Services\ContainerDisabledException::class );
-               $services->getService( 'Bar' );
-       }
-
-}
diff --git a/tests/phpunit/includes/libs/services/TestWiring1.php b/tests/phpunit/includes/libs/services/TestWiring1.php
deleted file mode 100644 (file)
index b6ff4eb..0000000
+++ /dev/null
@@ -1,10 +0,0 @@
-<?php
-/**
- * Test file for testing ServiceContainer::loadWiringFiles
- */
-
-return [
-       'Foo' => function () {
-               return 'Foo!';
-       },
-];
diff --git a/tests/phpunit/includes/libs/services/TestWiring2.php b/tests/phpunit/includes/libs/services/TestWiring2.php
deleted file mode 100644 (file)
index dfff64f..0000000
+++ /dev/null
@@ -1,10 +0,0 @@
-<?php
-/**
- * Test file for testing ServiceContainer::loadWiringFiles
- */
-
-return [
-       'Bar' => function () {
-               return 'Bar!';
-       },
-];
diff --git a/tests/phpunit/includes/libs/stats/PrefixingStatsdDataFactoryProxyTest.php b/tests/phpunit/includes/libs/stats/PrefixingStatsdDataFactoryProxyTest.php
deleted file mode 100644 (file)
index 46e23e3..0000000
+++ /dev/null
@@ -1,58 +0,0 @@
-<?php
-
-use Liuggio\StatsdClient\Factory\StatsdDataFactoryInterface;
-
-/**
- * @covers PrefixingStatsdDataFactoryProxy
- */
-class PrefixingStatsdDataFactoryProxyTest extends PHPUnit\Framework\TestCase {
-
-       use PHPUnit4And6Compat;
-
-       public function provideMethodNames() {
-               return [
-                       [ 'timing' ],
-                       [ 'gauge' ],
-                       [ 'set' ],
-                       [ 'increment' ],
-                       [ 'decrement' ],
-                       [ 'updateCount' ],
-                       [ 'produceStatsdData' ],
-               ];
-       }
-
-       /**
-        * @dataProvider provideMethodNames
-        */
-       public function testPrefixingAndPassthrough( $method ) {
-               /** @var StatsdDataFactoryInterface|PHPUnit_Framework_MockObject_MockObject $innerFactory */
-               $innerFactory = $this->getMock(
-                       \Liuggio\StatsdClient\Factory\StatsdDataFactoryInterface::class
-               );
-               $innerFactory->expects( $this->once() )
-                       ->method( $method )
-                       ->with( 'testprefix.metricname' );
-
-               $proxy = new PrefixingStatsdDataFactoryProxy( $innerFactory, 'testprefix' );
-               // 1,2,3,4 simply makes sure we provide enough parameters, without caring what they are
-               $proxy->$method( 'metricname', 1, 2, 3, 4 );
-       }
-
-       /**
-        * @dataProvider provideMethodNames
-        */
-       public function testPrefixIsTrimmed( $method ) {
-               /** @var StatsdDataFactoryInterface|PHPUnit_Framework_MockObject_MockObject $innerFactory */
-               $innerFactory = $this->getMock(
-                       \Liuggio\StatsdClient\Factory\StatsdDataFactoryInterface::class
-               );
-               $innerFactory->expects( $this->once() )
-                       ->method( $method )
-                       ->with( 'testprefix.metricname' );
-
-               $proxy = new PrefixingStatsdDataFactoryProxy( $innerFactory, 'testprefix...' );
-               // 1,2,3,4 simply makes sure we provide enough parameters, without caring what they are
-               $proxy->$method( 'metricname', 1, 2, 3, 4 );
-       }
-
-}
diff --git a/tests/phpunit/includes/media/GIFMetadataExtractorTest.php b/tests/phpunit/includes/media/GIFMetadataExtractorTest.php
deleted file mode 100644 (file)
index 278b441..0000000
+++ /dev/null
@@ -1,110 +0,0 @@
-<?php
-
-/**
- * @group Media
- */
-class GIFMetadataExtractorTest extends MediaWikiTestCase {
-
-       protected function setUp() {
-               parent::setUp();
-
-               $this->mediaPath = __DIR__ . '/../../data/media/';
-       }
-
-       /**
-        * Put in a file, and see if the metadata coming out is as expected.
-        * @param string $filename
-        * @param array $expected The extracted metadata.
-        * @dataProvider provideGetMetadata
-        * @covers GIFMetadataExtractor::getMetadata
-        */
-       public function testGetMetadata( $filename, $expected ) {
-               $actual = GIFMetadataExtractor::getMetadata( $this->mediaPath . $filename );
-               $this->assertEquals( $expected, $actual );
-       }
-
-       public static function provideGetMetadata() {
-               $xmpNugget = <<<EOF
-<?xpacket begin='' id='W5M0MpCehiHzreSzNTczkc9d'?>
-<x:xmpmeta xmlns:x='adobe:ns:meta/' x:xmptk='Image::ExifTool 7.30'>
-<rdf:RDF xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#'>
-
- <rdf:Description rdf:about=''
-  xmlns:Iptc4xmpCore='http://iptc.org/std/Iptc4xmpCore/1.0/xmlns/'>
-  <Iptc4xmpCore:Location>The interwebs</Iptc4xmpCore:Location>
- </rdf:Description>
-
- <rdf:Description rdf:about=''
-  xmlns:tiff='http://ns.adobe.com/tiff/1.0/'>
-  <tiff:Artist>Bawolff</tiff:Artist>
-  <tiff:ImageDescription>
-   <rdf:Alt>
-    <rdf:li xml:lang='x-default'>A file to test GIF</rdf:li>
-   </rdf:Alt>
-  </tiff:ImageDescription>
- </rdf:Description>
-</rdf:RDF>
-</x:xmpmeta>
-                                                                                                    
-                                                                                                    
-                                                                                                    
-                                                                                                    
-                                                                                                    
-                                                                                                    
-                                                                                                    
-                                                                                                    
-                                                                                                    
-                                                                                                    
-                                                                                                    
-                                                                                                    
-                                                                                                    
-                                                                                                    
-                                                                                                    
-                                                                                                    
-                                                                                                    
-                                                                                                    
-                                                                                                    
-                                                                                                    
-                                                                                                    
-                                                                                                    
-                                                                                                    
-                                                                                                    
-<?xpacket end='w'?>
-EOF;
-               $xmpNugget = str_replace( "\r", '', $xmpNugget ); // Windows compat
-
-               return [
-                       [
-                               'nonanimated.gif',
-                               [
-                                       'comment' => [ 'GIF test file ⁕ Created with GIMP' ],
-                                       'duration' => 0.1,
-                                       'frameCount' => 1,
-                                       'looped' => false,
-                                       'xmp' => '',
-                               ]
-                       ],
-                       [
-                               'animated.gif',
-                               [
-                                       'comment' => [ 'GIF test file . Created with GIMP' ],
-                                       'duration' => 2.4,
-                                       'frameCount' => 4,
-                                       'looped' => true,
-                                       'xmp' => '',
-                               ]
-                       ],
-
-                       [
-                               'animated-xmp.gif',
-                               [
-                                       'xmp' => $xmpNugget,
-                                       'duration' => 2.4,
-                                       'frameCount' => 4,
-                                       'looped' => true,
-                                       'comment' => [ 'GIƒ·test·file' ],
-                               ]
-                       ],
-               ];
-       }
-}
diff --git a/tests/phpunit/includes/media/IPTCTest.php b/tests/phpunit/includes/media/IPTCTest.php
deleted file mode 100644 (file)
index 4b3ba07..0000000
+++ /dev/null
@@ -1,85 +0,0 @@
-<?php
-
-/**
- * @group Media
- */
-class IPTCTest extends MediaWikiTestCase {
-
-       /**
-        * @covers IPTC::getCharset
-        */
-       public function testRecognizeUtf8() {
-               // utf-8 is the only one used in practise.
-               $res = IPTC::getCharset( "\x1b%G" );
-               $this->assertEquals( 'UTF-8', $res );
-       }
-
-       /**
-        * @covers IPTC::parse
-        */
-       public function testIPTCParseNoCharset88591() {
-               // basically IPTC for keyword with value of 0xBC which is 1/4 in iso-8859-1
-               // This data doesn't specify a charset. We're supposed to guess
-               // (which basically means utf-8 if valid, windows 1252 (iso 8859-1) if not)
-               $iptcData = "Photoshop 3.0\08BIM\4\4\0\0\0\0\0\x06\x1c\x02\x19\x00\x01\xBC";
-               $res = IPTC::parse( $iptcData );
-               $this->assertEquals( [ '¼' ], $res['Keywords'] );
-       }
-
-       /**
-        * @covers IPTC::parse
-        */
-       public function testIPTCParseNoCharset88591b() {
-               /* This one contains a sequence that's valid iso 8859-1 but not valid utf8 */
-               /* \xC3 = Ã, \xB8 = ¸  */
-               $iptcData = "Photoshop 3.0\08BIM\4\4\0\0\0\0\0\x09\x1c\x02\x19\x00\x04\xC3\xC3\xC3\xB8";
-               $res = IPTC::parse( $iptcData );
-               $this->assertEquals( [ 'ÃÃø' ], $res['Keywords'] );
-       }
-
-       /**
-        * Same as testIPTCParseNoCharset88591b, but forcing the charset to utf-8.
-        * What should happen is the first "\xC3\xC3" should be dropped as invalid,
-        * leaving \xC3\xB8, which is ø
-        * @covers IPTC::parse
-        */
-       public function testIPTCParseForcedUTFButInvalid() {
-               $iptcData = "Photoshop 3.0\08BIM\4\4\0\0\0\0\0\x11\x1c\x02\x19\x00\x04\xC3\xC3\xC3\xB8"
-                       . "\x1c\x01\x5A\x00\x03\x1B\x25\x47";
-               $res = IPTC::parse( $iptcData );
-               $this->assertEquals( [ 'ø' ], $res['Keywords'] );
-       }
-
-       /**
-        * @covers IPTC::parse
-        */
-       public function testIPTCParseNoCharsetUTF8() {
-               $iptcData = "Photoshop 3.0\08BIM\4\4\0\0\0\0\0\x07\x1c\x02\x19\x00\x02¼";
-               $res = IPTC::parse( $iptcData );
-               $this->assertEquals( [ '¼' ], $res['Keywords'] );
-       }
-
-       /**
-        * Testing something that has 2 values for keyword
-        * @covers IPTC::parse
-        */
-       public function testIPTCParseMulti() {
-               $iptcData = /* identifier */ "Photoshop 3.0\08BIM\4\4"
-                       /* length */ . "\0\0\0\0\0\x0D"
-                       . "\x1c\x02\x19" . "\x00\x01" . "\xBC"
-                       . "\x1c\x02\x19" . "\x00\x02" . "\xBC\xBD";
-               $res = IPTC::parse( $iptcData );
-               $this->assertEquals( [ '¼', '¼½' ], $res['Keywords'] );
-       }
-
-       /**
-        * @covers IPTC::parse
-        */
-       public function testIPTCParseUTF8() {
-               // This has the magic "\x1c\x01\x5A\x00\x03\x1B\x25\x47" which marks content as UTF8.
-               $iptcData =
-                       "Photoshop 3.0\08BIM\4\4\0\0\0\0\0\x0F\x1c\x02\x19\x00\x02¼\x1c\x01\x5A\x00\x03\x1B\x25\x47";
-               $res = IPTC::parse( $iptcData );
-               $this->assertEquals( [ '¼' ], $res['Keywords'] );
-       }
-}
diff --git a/tests/phpunit/includes/media/JpegMetadataExtractorTest.php b/tests/phpunit/includes/media/JpegMetadataExtractorTest.php
deleted file mode 100644 (file)
index c943cef..0000000
+++ /dev/null
@@ -1,128 +0,0 @@
-<?php
-/**
- * @todo Could use a test of extended XMP segments. Hard to find programs that
- * create example files, and creating my own in vim propbably wouldn't
- * serve as a very good "test". (Adobe photoshop probably creates such files
- * but it costs money). The implementation of it currently in MediaWiki is based
- * solely on reading the standard, without any real world test files.
- *
- * @group Media
- * @covers JpegMetadataExtractor
- */
-class JpegMetadataExtractorTest extends MediaWikiTestCase {
-
-       protected $filePath;
-
-       protected function setUp() {
-               parent::setUp();
-
-               $this->filePath = __DIR__ . '/../../data/media/';
-       }
-
-       /**
-        * We also use this test to test padding bytes don't
-        * screw stuff up
-        *
-        * @param string $file Filename
-        *
-        * @dataProvider provideUtf8Comment
-        */
-       public function testUtf8Comment( $file ) {
-               $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . $file );
-               $this->assertEquals( [ 'UTF-8 JPEG Comment — ¼' ], $res['COM'] );
-       }
-
-       public static function provideUtf8Comment() {
-               return [
-                       [ 'jpeg-comment-utf.jpg' ],
-                       [ 'jpeg-padding-even.jpg' ],
-                       [ 'jpeg-padding-odd.jpg' ],
-               ];
-       }
-
-       /** The file is iso-8859-1, but it should get auto converted */
-       public function testIso88591Comment() {
-               $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-comment-iso8859-1.jpg' );
-               $this->assertEquals( [ 'ISO-8859-1 JPEG Comment - ¼' ], $res['COM'] );
-       }
-
-       /** Comment values that are non-textual (random binary junk) should not be shown.
-        * The example test file has a comment with a 0x5 byte in it which is a control character
-        * and considered binary junk for our purposes.
-        */
-       public function testBinaryCommentStripped() {
-               $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-comment-binary.jpg' );
-               $this->assertEmpty( $res['COM'] );
-       }
-
-       /* Very rarely a file can have multiple comments.
-        *   Order of comments is based on order inside the file.
-        */
-       public function testMultipleComment() {
-               $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-comment-multiple.jpg' );
-               $this->assertEquals( [ 'foo', 'bar' ], $res['COM'] );
-       }
-
-       public function testXMPExtraction() {
-               $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-xmp-psir.jpg' );
-               $expected = file_get_contents( $this->filePath . 'jpeg-xmp-psir.xmp' );
-               $this->assertEquals( $expected, $res['XMP'] );
-       }
-
-       public function testPSIRExtraction() {
-               $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-xmp-psir.jpg' );
-               $expected = '50686f746f73686f7020332e30003842494d04040000000'
-                       . '000181c02190004746573741c02190003666f6f1c020000020004';
-               $this->assertEquals( $expected, bin2hex( $res['PSIR'][0] ) );
-       }
-
-       public function testXMPExtractionAltAppId() {
-               $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-xmp-alt.jpg' );
-               $expected = file_get_contents( $this->filePath . 'jpeg-xmp-psir.xmp' );
-               $this->assertEquals( $expected, $res['XMP'] );
-       }
-
-       public function testIPTCHashComparisionNoHash() {
-               $segments = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-xmp-psir.jpg' );
-               $res = JpegMetadataExtractor::doPSIR( $segments['PSIR'][0] );
-
-               $this->assertEquals( 'iptc-no-hash', $res );
-       }
-
-       public function testIPTCHashComparisionBadHash() {
-               $segments = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-iptc-bad-hash.jpg' );
-               $res = JpegMetadataExtractor::doPSIR( $segments['PSIR'][0] );
-
-               $this->assertEquals( 'iptc-bad-hash', $res );
-       }
-
-       public function testIPTCHashComparisionGoodHash() {
-               $segments = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-iptc-good-hash.jpg' );
-               $res = JpegMetadataExtractor::doPSIR( $segments['PSIR'][0] );
-
-               $this->assertEquals( 'iptc-good-hash', $res );
-       }
-
-       public function testExifByteOrder() {
-               $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'exif-user-comment.jpg' );
-               $expected = 'BE';
-               $this->assertEquals( $expected, $res['byteOrder'] );
-       }
-
-       public function testInfiniteRead() {
-               // test file truncated right after a segment, which previously
-               // caused an infinite loop looking for the next segment byte.
-               // Should get past infinite loop and throw in wfUnpack()
-               $this->setExpectedException( 'MWException' );
-               $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-segment-loop1.jpg' );
-       }
-
-       public function testInfiniteRead2() {
-               // test file truncated after a segment's marker and size, which
-               // would cause a seek past end of file. Seek past end of file
-               // doesn't actually fail, but prevents further reading and was
-               // devolving into the previous case (testInfiniteRead).
-               $this->setExpectedException( 'MWException' );
-               $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-segment-loop2.jpg' );
-       }
-}
diff --git a/tests/phpunit/includes/media/MediaHandlerTest.php b/tests/phpunit/includes/media/MediaHandlerTest.php
deleted file mode 100644 (file)
index 7a052f6..0000000
+++ /dev/null
@@ -1,68 +0,0 @@
-<?php
-
-/**
- * @group Media
- */
-class MediaHandlerTest extends MediaWikiTestCase {
-
-       /**
-        * @covers MediaHandler::fitBoxWidth
-        *
-        * @dataProvider provideTestFitBoxWidth
-        */
-       public function testFitBoxWidth( $width, $height, $max, $expected ) {
-               $y = round( $expected * $height / $width );
-               $result = MediaHandler::fitBoxWidth( $width, $height, $max );
-               $y2 = round( $result * $height / $width );
-               $this->assertEquals( $expected,
-                       $result,
-                       "($width, $height, $max) wanted: {$expected}x$y, got: {z$result}x$y2" );
-       }
-
-       public static function provideTestFitBoxWidth() {
-               return array_merge(
-                       static::generateTestFitBoxWidthData( 50, 50, [
-                                       50 => 50,
-                                       17 => 17,
-                                       18 => 18 ]
-                       ),
-                       static::generateTestFitBoxWidthData( 366, 300, [
-                                       50 => 61,
-                                       17 => 21,
-                                       18 => 22 ]
-                       ),
-                       static::generateTestFitBoxWidthData( 300, 366, [
-                                       50 => 41,
-                                       17 => 14,
-                                       18 => 15 ]
-                       ),
-                       static::generateTestFitBoxWidthData( 100, 400, [
-                                       50 => 12,
-                                       17 => 4,
-                                       18 => 4 ]
-                       )
-               );
-       }
-
-       /**
-        * Generate single test cases by combining the dimensions and tests contents
-        *
-        * It creates:
-        * [$width, $height, $max, $expected],
-        * [$width, $height, $max2, $expected2], ...
-        * out of parameters:
-        * $width, $height, { $max => $expected, $max2 => $expected2, ... }
-        *
-        * @param int $width
-        * @param int $height
-        * @param array $tests associative array of $max => $expected values
-        * @return array
-        */
-       private static function generateTestFitBoxWidthData( $width, $height, $tests ) {
-               $result = [];
-               foreach ( $tests as $max => $expected ) {
-                       $result[] = [ $width, $height, $max, $expected ];
-               }
-               return $result;
-       }
-}
diff --git a/tests/phpunit/includes/media/SVGMetadataExtractorTest.php b/tests/phpunit/includes/media/SVGMetadataExtractorTest.php
deleted file mode 100644 (file)
index 6b94d0a..0000000
+++ /dev/null
@@ -1,201 +0,0 @@
-<?php
-
-/**
- * @group Media
- * @covers SVGMetadataExtractor
- */
-class SVGMetadataExtractorTest extends MediaWikiTestCase {
-
-       /**
-        * @dataProvider provideSvgFiles
-        */
-       public function testGetMetadata( $infile, $expected ) {
-               $this->assertMetadata( $infile, $expected );
-       }
-
-       /**
-        * @dataProvider provideSvgFilesWithXMLMetadata
-        */
-       public function testGetXMLMetadata( $infile, $expected ) {
-               $r = new XMLReader();
-               $this->assertMetadata( $infile, $expected );
-       }
-
-       /**
-        * @dataProvider provideSvgUnits
-        */
-       public function testScaleSVGUnit( $inUnit, $expected ) {
-               $this->assertEquals(
-                       $expected,
-                       SVGReader::scaleSVGUnit( $inUnit ),
-                       'SVG unit conversion and scaling failure'
-               );
-       }
-
-       function assertMetadata( $infile, $expected ) {
-               try {
-                       $data = SVGMetadataExtractor::getMetadata( $infile );
-                       $this->assertEquals( $expected, $data, 'SVG metadata extraction test' );
-               } catch ( MWException $e ) {
-                       if ( $expected === false ) {
-                               $this->assertTrue( true, 'SVG metadata extracted test (expected failure)' );
-                       } else {
-                               throw $e;
-                       }
-               }
-       }
-
-       public static function provideSvgFiles() {
-               $base = __DIR__ . '/../../data/media';
-
-               return [
-                       [
-                               "$base/Wikimedia-logo.svg",
-                               [
-                                       'width' => 1024,
-                                       'height' => 1024,
-                                       'originalWidth' => '1024',
-                                       'originalHeight' => '1024',
-                                       'translations' => [],
-                               ]
-                       ],
-                       [
-                               "$base/QA_icon.svg",
-                               [
-                                       'width' => 60,
-                                       'height' => 60,
-                                       'originalWidth' => '60',
-                                       'originalHeight' => '60',
-                                       'translations' => [],
-                               ]
-                       ],
-                       [
-                               "$base/Gtk-media-play-ltr.svg",
-                               [
-                                       'width' => 60,
-                                       'height' => 60,
-                                       'originalWidth' => '60.0000000',
-                                       'originalHeight' => '60.0000000',
-                                       'translations' => [],
-                               ]
-                       ],
-                       [
-                               "$base/Toll_Texas_1.svg",
-                               // This file triggered T33719, needs entity expansion in the xmlns checks
-                               [
-                                       'width' => 385,
-                                       'height' => 385,
-                                       'originalWidth' => '385',
-                                       'originalHeight' => '385.0004883',
-                                       'translations' => [],
-                               ]
-                       ],
-                       [
-                               "$base/Tux.svg",
-                               [
-                                       'width' => 512,
-                                       'height' => 594,
-                                       'originalWidth' => '100%',
-                                       'originalHeight' => '100%',
-                                       'title' => 'Tux',
-                                       'translations' => [],
-                                       'description' => 'For more information see: http://commons.wikimedia.org/wiki/Image:Tux.svg',
-                               ]
-                       ],
-                       [
-                               "$base/Speech_bubbles.svg",
-                               [
-                                       'width' => 627,
-                                       'height' => 461,
-                                       'originalWidth' => '17.7cm',
-                                       'originalHeight' => '13cm',
-                                       'translations' => [
-                                               'de' => SVGReader::LANG_FULL_MATCH,
-                                               'fr' => SVGReader::LANG_FULL_MATCH,
-                                               'nl' => SVGReader::LANG_FULL_MATCH,
-                                               'tlh-ca' => SVGReader::LANG_FULL_MATCH,
-                                               'tlh' => SVGReader::LANG_PREFIX_MATCH
-                                       ],
-                               ]
-                       ],
-                       [
-                               "$base/Soccer_ball_animated.svg",
-                               [
-                                       'width' => 150,
-                                       'height' => 150,
-                                       'originalWidth' => '150',
-                                       'originalHeight' => '150',
-                                       'animated' => true,
-                                       'translations' => []
-                               ],
-                       ],
-                       [
-                               "$base/comma_separated_viewbox.svg",
-                               [
-                                       'width' => 512,
-                                       'height' => 594,
-                                       'originalWidth' => '100%',
-                                       'originalHeight' => '100%',
-                                       'translations' => []
-                               ],
-                       ],
-               ];
-       }
-
-       public static function provideSvgFilesWithXMLMetadata() {
-               $base = __DIR__ . '/../../data/media';
-               // phpcs:disable Generic.Files.LineLength
-               $metadata = '<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
-      <ns4:Work xmlns:ns4="http://creativecommons.org/ns#" rdf:about="">
-        <ns5:format xmlns:ns5="http://purl.org/dc/elements/1.1/">image/svg+xml</ns5:format>
-        <ns5:type xmlns:ns5="http://purl.org/dc/elements/1.1/" rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
-      </ns4:Work>
-    </rdf:RDF>';
-               // phpcs:enable
-
-               $metadata = str_replace( "\r", '', $metadata ); // Windows compat
-               return [
-                       [
-                               "$base/US_states_by_total_state_tax_revenue.svg",
-                               [
-                                       'height' => 593,
-                                       'metadata' => $metadata,
-                                       'width' => 959,
-                                       'originalWidth' => '958.69',
-                                       'originalHeight' => '592.78998',
-                                       'translations' => [],
-                               ]
-                       ],
-               ];
-       }
-
-       public static function provideSvgUnits() {
-               return [
-                       [ '1' , 1 ],
-                       [ '1.1' , 1.1 ],
-                       [ '0.1' , 0.1 ],
-                       [ '.1' , 0.1 ],
-                       [ '1e2' , 100 ],
-                       [ '1E2' , 100 ],
-                       [ '+1' , 1 ],
-                       [ '-1' , -1 ],
-                       [ '-1.1' , -1.1 ],
-                       [ '1e+2' , 100 ],
-                       [ '1e-2' , 0.01 ],
-                       [ '10px' , 10 ],
-                       [ '10pt' , 10 * 1.25 ],
-                       [ '10pc' , 10 * 15 ],
-                       [ '10mm' , 10 * 3.543307 ],
-                       [ '10cm' , 10 * 35.43307 ],
-                       [ '10in' , 10 * 90 ],
-                       [ '10em' , 10 * 16 ],
-                       [ '10ex' , 10 * 12 ],
-                       [ '10%' , 51.2 ],
-                       [ '10 px' , 10 ],
-                       // Invalid values
-                       [ '1e1.1', 10 ],
-                       [ '10bp', 10 ],
-                       [ 'p10', null ],
-               ];
-       }
-}
diff --git a/tests/phpunit/includes/media/WebPHandlerTest.php b/tests/phpunit/includes/media/WebPHandlerTest.php
deleted file mode 100644 (file)
index ac0ad98..0000000
+++ /dev/null
@@ -1,151 +0,0 @@
-<?php
-
-/**
- * @covers WebPHandler
- */
-class WebPHandlerTest extends MediaWikiTestCase {
-       public function setUp() {
-               parent::setUp();
-               // Allocated file for testing
-               $this->tempFileName = tempnam( wfTempDir(), 'WEBP' );
-       }
-
-       public function tearDown() {
-               parent::tearDown();
-               unlink( $this->tempFileName );
-       }
-
-       /**
-        * @dataProvider provideTestExtractMetaData
-        */
-       public function testExtractMetaData( $header, $expectedResult ) {
-               // Put header into file
-               file_put_contents( $this->tempFileName, $header );
-
-               $this->assertEquals( $expectedResult, WebPHandler::extractMetadata( $this->tempFileName ) );
-       }
-
-       public function provideTestExtractMetaData() {
-               // phpcs:disable Generic.Files.LineLength
-               return [
-                       // Files from https://developers.google.com/speed/webp/gallery2
-                       [ "\x52\x49\x46\x46\x90\x68\x01\x00\x57\x45\x42\x50\x56\x50\x38\x4C\x83\x68\x01\x00\x2F\x8F\x01\x4B\x10\x8D\x38\x6C\xDB\x46\x92\xE0\xE0\x82\x7B\x6C",
-                               [ 'compression' => 'lossless', 'width' => 400, 'height' => 301 ] ],
-                       [ "\x52\x49\x46\x46\x64\x5B\x00\x00\x57\x45\x42\x50\x56\x50\x38\x58\x0A\x00\x00\x00\x10\x00\x00\x00\x8F\x01\x00\x2C\x01\x00\x41\x4C\x50\x48\xE5\x0E",
-                               [ 'compression' => 'unknown', 'animated' => false, 'transparency' => true, 'width' => 400, 'height' => 301 ] ],
-                       [ "\x52\x49\x46\x46\xA8\x72\x00\x00\x57\x45\x42\x50\x56\x50\x38\x4C\x9B\x72\x00\x00\x2F\x81\x81\x62\x10\x8D\x40\x8C\x24\x39\x6E\x73\x73\x38\x01\x96",
-                               [ 'compression' => 'lossless', 'width' => 386, 'height' => 395 ] ],
-                       [ "\x52\x49\x46\x46\xE0\x42\x00\x00\x57\x45\x42\x50\x56\x50\x38\x58\x0A\x00\x00\x00\x10\x00\x00\x00\x81\x01\x00\x8A\x01\x00\x41\x4C\x50\x48\x56\x10",
-                               [ 'compression' => 'unknown', 'animated' => false, 'transparency' => true, 'width' => 386, 'height' => 395 ] ],
-                       [ "\x52\x49\x46\x46\x70\x61\x02\x00\x57\x45\x42\x50\x56\x50\x38\x4C\x63\x61\x02\x00\x2F\x1F\xC3\x95\x10\x8D\xC8\x72\xDB\xC8\x92\x24\xD8\x91\xD9\x91",
-                               [ 'compression' => 'lossless', 'width' => 800, 'height' => 600 ] ],
-                       [ "\x52\x49\x46\x46\x1C\x1D\x01\x00\x57\x45\x42\x50\x56\x50\x38\x58\x0A\x00\x00\x00\x10\x00\x00\x00\x1F\x03\x00\x57\x02\x00\x41\x4C\x50\x48\x25\x8B",
-                               [ 'compression' => 'unknown', 'animated' => false, 'transparency' => true, 'width' => 800, 'height' => 600 ] ],
-                       [ "\x52\x49\x46\x46\xFA\xC5\x00\x00\x57\x45\x42\x50\x56\x50\x38\x4C\xEE\xC5\x00\x00\x2F\xA4\x81\x28\x10\x8D\x40\x68\x24\xC9\x91\xA4\xAE\xF3\x97\x75",
-                               [ 'compression' => 'lossless', 'width' => 421, 'height' => 163 ] ],
-                       [ "\x52\x49\x46\x46\xF6\x5D\x00\x00\x57\x45\x42\x50\x56\x50\x38\x58\x0A\x00\x00\x00\x10\x00\x00\x00\xA4\x01\x00\xA2\x00\x00\x41\x4C\x50\x48\x38\x1A",
-                               [ 'compression' => 'unknown', 'animated' => false, 'transparency' => true, 'width' => 421, 'height' => 163 ] ],
-                       [ "\x52\x49\x46\x46\xC4\x96\x01\x00\x57\x45\x42\x50\x56\x50\x38\x4C\xB8\x96\x01\x00\x2F\x2B\xC1\x4A\x10\x11\x87\x6D\xDB\x48\x12\xFC\x60\xB0\x83\x24",
-                               [ 'compression' => 'lossless', 'width' => 300, 'height' => 300 ] ],
-                       [ "\x52\x49\x46\x46\x0A\x11\x01\x00\x57\x45\x42\x50\x56\x50\x38\x58\x0A\x00\x00\x00\x10\x00\x00\x00\x2B\x01\x00\x2B\x01\x00\x41\x4C\x50\x48\x67\x6E",
-                               [ 'compression' => 'unknown', 'animated' => false, 'transparency' => true, 'width' => 300, 'height' => 300 ] ],
-
-                       // Lossy files from https://developers.google.com/speed/webp/gallery1
-                       [ "\x52\x49\x46\x46\x68\x76\x00\x00\x57\x45\x42\x50\x56\x50\x38\x20\x5C\x76\x00\x00\xD2\xBE\x01\x9D\x01\x2A\x26\x02\x70\x01\x3E\xD5\x4E\x97\x43\xA2",
-                               [ 'compression' => 'lossy', 'width' => 550, 'height' => 368 ] ],
-                       [ "\x52\x49\x46\x46\xB0\xEC\x00\x00\x57\x45\x42\x50\x56\x50\x38\x20\xA4\xEC\x00\x00\xB2\x4B\x02\x9D\x01\x2A\x26\x02\x94\x01\x3E\xD1\x50\x96\x46\x26",
-                               [ 'compression' => 'lossy', 'width' => 550, 'height' => 404 ] ],
-                       [ "\x52\x49\x46\x46\x7A\x19\x03\x00\x57\x45\x42\x50\x56\x50\x38\x20\x6E\x19\x03\x00\xB2\xF8\x09\x9D\x01\x2A\x00\x05\xD0\x02\x3E\xAD\x46\x99\x4A\xA5",
-                               [ 'compression' => 'lossy', 'width' => 1280, 'height' => 720 ] ],
-                       [ "\x52\x49\x46\x46\x44\xB3\x02\x00\x57\x45\x42\x50\x56\x50\x38\x20\x38\xB3\x02\x00\x52\x57\x06\x9D\x01\x2A\x00\x04\x04\x03\x3E\xA5\x44\x96\x49\x26",
-                               [ 'compression' => 'lossy', 'width' => 1024, 'height' => 772 ] ],
-                       [ "\x52\x49\x46\x46\x02\x43\x01\x00\x57\x45\x42\x50\x56\x50\x38\x20\xF6\x42\x01\x00\x12\xC0\x05\x9D\x01\x2A\x00\x04\xF0\x02\x3E\x79\x34\x93\x47\xA4",
-                               [ 'compression' => 'lossy', 'width' => 1024, 'height' => 752 ] ],
-
-                       // Animated file from https://groups.google.com/a/chromium.org/d/topic/blink-dev/Y8tRC4mdQz8/discussion
-                       [ "\x52\x49\x46\x46\xD0\x0B\x02\x00\x57\x45\x42\x50\x56\x50\x38\x58\x0A\x00\x00\x00\x12\x00\x00\x00\x3F\x01\x00\x3F\x01\x00\x41\x4E",
-                               [ 'compression' => 'unknown', 'animated' => true, 'transparency' => true, 'width' => 320, 'height' => 320 ] ],
-
-                       // Error cases
-                       [ '', false ],
-                       [ '                                    ', false ],
-                       [ 'RIFF                                ', false ],
-                       [ 'RIFF1234WEBP                        ', false ],
-                       [ 'RIFF1234WEBPVP8                     ', false ],
-                       [ 'RIFF1234WEBPVP8L                    ', false ],
-               ];
-               // phpcs:enable
-       }
-
-       /**
-        * @dataProvider provideTestWithFileExtractMetaData
-        */
-       public function testWithFileExtractMetaData( $filename, $expectedResult ) {
-               $this->assertEquals( $expectedResult, WebPHandler::extractMetadata( $filename ) );
-       }
-
-       public function provideTestWithFileExtractMetaData() {
-               return [
-                       [ __DIR__ . '/../../data/media/2_webp_ll.webp',
-                               [
-                                       'compression' => 'lossless',
-                                       'width' => 386,
-                                       'height' => 395
-                               ]
-                       ],
-                       [ __DIR__ . '/../../data/media/2_webp_a.webp',
-                               [
-                                       'compression' => 'lossy',
-                                       'animated' => false,
-                                       'transparency' => true,
-                                       'width' => 386,
-                                       'height' => 395
-                               ]
-                       ],
-               ];
-       }
-
-       /**
-        * @dataProvider provideTestGetImageSize
-        */
-       public function testGetImageSize( $path, $expectedResult ) {
-               $handler = new WebPHandler();
-               $this->assertEquals( $expectedResult, $handler->getImageSize( null, $path ) );
-       }
-
-       public function provideTestGetImageSize() {
-               return [
-                       // Public domain files from https://developers.google.com/speed/webp/gallery2
-                       [ __DIR__ . '/../../data/media/2_webp_a.webp', [ 386, 395 ] ],
-                       [ __DIR__ . '/../../data/media/2_webp_ll.webp', [ 386, 395 ] ],
-                       [ __DIR__ . '/../../data/media/webp_animated.webp', [ 300, 225 ] ],
-
-                       // Error cases
-                       [ __FILE__, false ],
-               ];
-       }
-
-       /**
-        * Tests the WebP MIME detection. This should really be a separate test, but sticking it
-        * here for now.
-        *
-        * @dataProvider provideTestGetMimeType
-        */
-       public function testGuessMimeType( $path ) {
-               $mime = MediaWiki\MediaWikiServices::getInstance()->getMimeAnalyzer();
-               $this->assertEquals( 'image/webp', $mime->guessMimeType( $path, false ) );
-       }
-
-       public function provideTestGetMimeType() {
-               return [
-                               // Public domain files from https://developers.google.com/speed/webp/gallery2
-                               [ __DIR__ . '/../../data/media/2_webp_a.webp' ],
-                               [ __DIR__ . '/../../data/media/2_webp_ll.webp' ],
-                               [ __DIR__ . '/../../data/media/webp_animated.webp' ],
-               ];
-       }
-}
-
-/* Python code to extract a header and convert to PHP format:
- * print '"%s"' % ''.implode( '\\x%02X' % ord(c) for c in urllib.urlopen(url).read(36) )
- */
diff --git a/tests/phpunit/includes/objectcache/MemcachedBagOStuffTest.php b/tests/phpunit/includes/objectcache/MemcachedBagOStuffTest.php
deleted file mode 100644 (file)
index 45971da..0000000
+++ /dev/null
@@ -1,107 +0,0 @@
-<?php
-/**
- * @group BagOStuff
- */
-class MemcachedBagOStuffTest extends MediaWikiTestCase {
-       /** @var MemcachedBagOStuff */
-       private $cache;
-
-       protected function setUp() {
-               parent::setUp();
-               $this->cache = new MemcachedPhpBagOStuff( [ 'keyspace' => 'test', 'servers' => [] ] );
-       }
-
-       /**
-        * @covers MemcachedBagOStuff::makeKey
-        */
-       public function testKeyNormalization() {
-               $this->assertEquals(
-                       'test:vanilla',
-                       $this->cache->makeKey( 'vanilla' )
-               );
-
-               $this->assertEquals(
-                       'test:punctuation_marks_are_ok:!@$^&*()',
-                       $this->cache->makeKey( 'punctuation_marks_are_ok', '!@$^&*()' )
-               );
-
-               $this->assertEquals(
-                       'test:but_spaces:hashes%23:and%0Anewlines:are_not',
-                       $this->cache->makeKey( 'but spaces', 'hashes#', "and\nnewlines", 'are_not' )
-               );
-
-               $this->assertEquals(
-                       'test:this:key:contains:%F0%9D%95%9E%F0%9D%95%A6%F0%9D%95%9D%F0%9D%95%A5%F0%9' .
-                               'D%95%9A%F0%9D%95%93%F0%9D%95%AA%F0%9D%95%A5%F0%9D%95%96:characters',
-                       $this->cache->makeKey( 'this', 'key', 'contains', '𝕞𝕦𝕝𝕥𝕚𝕓𝕪𝕥𝕖', 'characters' )
-               );
-
-               $this->assertEquals(
-                       'test:this:key:contains:#c118f92685a635cb843039de50014c9c',
-                       $this->cache->makeKey( 'this', 'key', 'contains', '𝕥𝕠𝕠 𝕞𝕒𝕟𝕪 𝕞𝕦𝕝𝕥𝕚𝕓𝕪𝕥𝕖 𝕔𝕙𝕒𝕣𝕒𝕔𝕥𝕖𝕣𝕤' )
-               );
-
-               $this->assertEquals(
-                       'test:BagOStuff-long-key:##dc89dcb43b28614da27660240af478b5',
-                       $this->cache->makeKey( '𝕖𝕧𝕖𝕟', '𝕚𝕗', '𝕨𝕖', '𝕄𝔻𝟝', '𝕖𝕒𝕔𝕙',
-                               '𝕒𝕣𝕘𝕦𝕞𝕖𝕟𝕥', '𝕥𝕙𝕚𝕤', '𝕜𝕖𝕪', '𝕨𝕠𝕦𝕝𝕕', '𝕤𝕥𝕚𝕝𝕝', '𝕓𝕖', '𝕥𝕠𝕠', '𝕝𝕠𝕟𝕘' )
-               );
-
-               $this->assertEquals(
-                       'test:%23%235820ad1d105aa4dc698585c39df73e19',
-                       $this->cache->makeKey( '##5820ad1d105aa4dc698585c39df73e19' )
-               );
-
-               $this->assertEquals(
-                       'test:percent_is_escaped:!@$%25^&*()',
-                       $this->cache->makeKey( 'percent_is_escaped', '!@$%^&*()' )
-               );
-
-               $this->assertEquals(
-                       'test:colon_is_escaped:!@$%3A^&*()',
-                       $this->cache->makeKey( 'colon_is_escaped', '!@$:^&*()' )
-               );
-
-               $this->assertEquals(
-                       'test:long_key_part_hashed:#0244f7b1811d982dd932dd7de01465ac',
-                       $this->cache->makeKey( 'long_key_part_hashed', str_repeat( 'y', 500 ) )
-               );
-       }
-
-       /**
-        * @dataProvider validKeyProvider
-        * @covers MemcachedBagOStuff::validateKeyEncoding
-        */
-       public function testValidateKeyEncoding( $key ) {
-               $this->assertSame( $key, $this->cache->validateKeyEncoding( $key ) );
-       }
-
-       public function validKeyProvider() {
-               return [
-                       'empty' => [ '' ],
-                       'digits' => [ '09' ],
-                       'letters' => [ 'AZaz' ],
-                       'ASCII special characters' => [ '!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~' ],
-               ];
-       }
-
-       /**
-        * @dataProvider invalidKeyProvider
-        * @covers MemcachedBagOStuff::validateKeyEncoding
-        */
-       public function testValidateKeyEncodingThrowsException( $key ) {
-               $this->setExpectedException( Exception::class );
-               $this->cache->validateKeyEncoding( $key );
-       }
-
-       public function invalidKeyProvider() {
-               return [
-                       [ "\x00" ],
-                       [ ' ' ],
-                       [ "\x1F" ],
-                       [ "\x7F" ],
-                       [ "\x80" ],
-                       [ "\xFF" ],
-               ];
-       }
-}
diff --git a/tests/phpunit/includes/objectcache/RESTBagOStuffTest.php b/tests/phpunit/includes/objectcache/RESTBagOStuffTest.php
deleted file mode 100644 (file)
index dfbca70..0000000
+++ /dev/null
@@ -1,96 +0,0 @@
-<?php
-/**
- * @group BagOStuff
- *
- * @covers RESTBagOStuff
- */
-class RESTBagOStuffTest extends MediaWikiTestCase {
-
-       /**
-        * @var MultiHttpClient
-        */
-       private $client;
-       /**
-        * @var RESTBagOStuff
-        */
-       private $bag;
-
-       public function setUp() {
-               parent::setUp();
-               $this->client =
-                       $this->getMockBuilder( MultiHttpClient::class )
-                               ->setConstructorArgs( [ [] ] )
-                               ->setMethods( [ 'run' ] )
-                               ->getMock();
-               $this->bag = new RESTBagOStuff( [ 'client' => $this->client, 'url' => 'http://test/rest/' ] );
-       }
-
-       public function testGet() {
-               $this->client->expects( $this->once() )->method( 'run' )->with( [
-                       'method' => 'GET',
-                       'url' => 'http://test/rest/42xyz42',
-                       'headers' => []
-                       // list( $rcode, $rdesc, $rhdrs, $rbody, $rerr )
-               ] )->willReturn( [ 200, 'OK', [], '"somedata"', 0 ] );
-               $result = $this->bag->get( '42xyz42' );
-               $this->assertEquals( 'somedata', $result );
-       }
-
-       public function testGetNotExist() {
-               $this->client->expects( $this->once() )->method( 'run' )->with( [
-                       'method' => 'GET',
-                       'url' => 'http://test/rest/42xyz42',
-                       'headers' => []
-                       // list( $rcode, $rdesc, $rhdrs, $rbody, $rerr )
-               ] )->willReturn( [ 404, 'Not found', [], 'Nothing to see here', 0 ] );
-               $result = $this->bag->get( '42xyz42' );
-               $this->assertFalse( $result );
-       }
-
-       public function testGetBadClient() {
-               $this->client->expects( $this->once() )->method( 'run' )->with( [
-                       'method' => 'GET',
-                       'url' => 'http://test/rest/42xyz42',
-                       'headers' => []
-                       // list( $rcode, $rdesc, $rhdrs, $rbody, $rerr )
-               ] )->willReturn( [ 0, '', [], '', 'cURL has failed you today' ] );
-               $result = $this->bag->get( '42xyz42' );
-               $this->assertFalse( $result );
-               $this->assertEquals( BagOStuff::ERR_UNREACHABLE, $this->bag->getLastError() );
-       }
-
-       public function testGetBadServer() {
-               $this->client->expects( $this->once() )->method( 'run' )->with( [
-                       'method' => 'GET',
-                       'url' => 'http://test/rest/42xyz42',
-                       'headers' => []
-                       // list( $rcode, $rdesc, $rhdrs, $rbody, $rerr )
-               ] )->willReturn( [ 500, 'Too busy', [], 'Server is too busy', '' ] );
-               $result = $this->bag->get( '42xyz42' );
-               $this->assertFalse( $result );
-               $this->assertEquals( BagOStuff::ERR_UNEXPECTED, $this->bag->getLastError() );
-       }
-
-       public function testPut() {
-               $this->client->expects( $this->once() )->method( 'run' )->with( [
-                       'method' => 'PUT',
-                       'url' => 'http://test/rest/42xyz42',
-                       'body' => '"postdata"',
-                       'headers' => []
-                       // list( $rcode, $rdesc, $rhdrs, $rbody, $rerr )
-               ] )->willReturn( [ 200, 'OK', [], 'Done', 0 ] );
-               $result = $this->bag->set( '42xyz42', 'postdata' );
-               $this->assertTrue( $result );
-       }
-
-       public function testDelete() {
-               $this->client->expects( $this->once() )->method( 'run' )->with( [
-                       'method' => 'DELETE',
-                       'url' => 'http://test/rest/42xyz42',
-                       'headers' => []
-                       // list( $rcode, $rdesc, $rhdrs, $rbody, $rerr )
-               ] )->willReturn( [ 200, 'OK', [], 'Done', 0 ] );
-               $result = $this->bag->delete( '42xyz42' );
-               $this->assertTrue( $result );
-       }
-}
diff --git a/tests/phpunit/includes/objectcache/RedisBagOStuffTest.php b/tests/phpunit/includes/objectcache/RedisBagOStuffTest.php
deleted file mode 100644 (file)
index df5614d..0000000
+++ /dev/null
@@ -1,110 +0,0 @@
-<?php
-
-use Wikimedia\TestingAccessWrapper;
-
-/**
- * @group BagOStuff
- */
-class RedisBagOStuffTest extends PHPUnit\Framework\TestCase {
-
-       use MediaWikiCoversValidator;
-
-       /** @var RedisBagOStuff */
-       private $cache;
-
-       protected function setUp() {
-               parent::setUp();
-               $cache = $this->getMockBuilder( RedisBagOStuff::class )
-                       ->disableOriginalConstructor()
-                       ->getMock();
-               $this->cache = TestingAccessWrapper::newFromObject( $cache );
-       }
-
-       /**
-        * @covers RedisBagOStuff::unserialize
-        * @dataProvider unserializeProvider
-        */
-       public function testUnserialize( $expected, $input, $message ) {
-               $actual = $this->cache->unserialize( $input );
-               $this->assertSame( $expected, $actual, $message );
-       }
-
-       public function unserializeProvider() {
-               return [
-                       [
-                               -1,
-                               '-1',
-                               'String representation of \'-1\'',
-                       ],
-                       [
-                               0,
-                               '0',
-                               'String representation of \'0\'',
-                       ],
-                       [
-                               1,
-                               '1',
-                               'String representation of \'1\'',
-                       ],
-                       [
-                               -1.0,
-                               'd:-1;',
-                               'Serialized negative double',
-                       ],
-                       [
-                               'foo',
-                               's:3:"foo";',
-                               'Serialized string',
-                       ]
-               ];
-       }
-
-       /**
-        * @covers RedisBagOStuff::serialize
-        * @dataProvider serializeProvider
-        */
-       public function testSerialize( $expected, $input, $message ) {
-               $actual = $this->cache->serialize( $input );
-               $this->assertSame( $expected, $actual, $message );
-       }
-
-       public function serializeProvider() {
-               return [
-                       [
-                               -1,
-                               -1,
-                               '-1 as integer',
-                       ],
-                       [
-                               0,
-                               0,
-                               '0 as integer',
-                       ],
-                       [
-                               1,
-                               1,
-                               '1 as integer',
-                       ],
-                       [
-                               'd:-1;',
-                               -1.0,
-                               'Negative double',
-                       ],
-                       [
-                               's:3:"2.1";',
-                               '2.1',
-                               'Decimal string',
-                       ],
-                       [
-                               's:1:"1";',
-                               '1',
-                               'String representation of 1',
-                       ],
-                       [
-                               's:3:"foo";',
-                               'foo',
-                               'String',
-                       ],
-               ];
-       }
-}
diff --git a/tests/phpunit/includes/page/ArticleTest.php b/tests/phpunit/includes/page/ArticleTest.php
deleted file mode 100644 (file)
index df4a281..0000000
+++ /dev/null
@@ -1,57 +0,0 @@
-<?php
-
-class ArticleTest extends MediaWikiTestCase {
-
-       /**
-        * @var Title
-        */
-       private $title;
-       /**
-        * @var Article
-        */
-       private $article;
-
-       /** creates a title object and its article object */
-       protected function setUp() {
-               parent::setUp();
-               $this->title = Title::makeTitle( NS_MAIN, 'SomePage' );
-               $this->article = new Article( $this->title );
-       }
-
-       /** cleanup title object and its article object */
-       protected function tearDown() {
-               parent::tearDown();
-               $this->title = null;
-               $this->article = null;
-       }
-
-       /**
-        * @covers Article::__get
-        */
-       public function testImplementsGetMagic() {
-               $this->assertEquals( false, $this->article->mLatest, "Article __get magic" );
-       }
-
-       /**
-        * @depends testImplementsGetMagic
-        * @covers Article::__set
-        */
-       public function testImplementsSetMagic() {
-               $this->article->mLatest = 2;
-               $this->assertEquals( 2, $this->article->mLatest, "Article __set magic" );
-       }
-
-       /**
-        * @covers Article::__get
-        * @covers Article::__set
-        */
-       public function testGetOrSetOnNewProperty() {
-               $this->article->ext_someNewProperty = 12;
-               $this->assertEquals( 12, $this->article->ext_someNewProperty,
-                       "Article get/set magic on new field" );
-
-               $this->article->ext_someNewProperty = -8;
-               $this->assertEquals( -8, $this->article->ext_someNewProperty,
-                       "Article get/set magic on update to new field" );
-       }
-}
diff --git a/tests/phpunit/includes/parser/ParserPreloadTest.php b/tests/phpunit/includes/parser/ParserPreloadTest.php
deleted file mode 100644 (file)
index 560b921..0000000
+++ /dev/null
@@ -1,97 +0,0 @@
-<?php
-
-use MediaWiki\MediaWikiServices;
-
-/**
- * Basic tests for Parser::getPreloadText
- * @author Antoine Musso
- *
- * @covers Parser
- * @covers StripState
- *
- * @covers Preprocessor_DOM
- * @covers PPDStack
- * @covers PPDStackElement
- * @covers PPDPart
- * @covers PPFrame_DOM
- * @covers PPTemplateFrame_DOM
- * @covers PPCustomFrame_DOM
- * @covers PPNode_DOM
- *
- * @covers Preprocessor_Hash
- * @covers PPDStack_Hash
- * @covers PPDStackElement_Hash
- * @covers PPDPart_Hash
- * @covers PPFrame_Hash
- * @covers PPTemplateFrame_Hash
- * @covers PPCustomFrame_Hash
- * @covers PPNode_Hash_Tree
- * @covers PPNode_Hash_Text
- * @covers PPNode_Hash_Array
- * @covers PPNode_Hash_Attr
- */
-class ParserPreloadTest extends MediaWikiTestCase {
-       /**
-        * @var Parser
-        */
-       private $testParser;
-       /**
-        * @var ParserOptions
-        */
-       private $testParserOptions;
-       /**
-        * @var Title
-        */
-       private $title;
-
-       protected function setUp() {
-               parent::setUp();
-               $this->testParserOptions = ParserOptions::newFromUserAndLang( new User,
-                       MediaWikiServices::getInstance()->getContentLanguage() );
-
-               $this->testParser = new Parser();
-               $this->testParser->Options( $this->testParserOptions );
-               $this->testParser->clearState();
-
-               $this->title = Title::newFromText( 'Preload Test' );
-       }
-
-       protected function tearDown() {
-               parent::tearDown();
-
-               unset( $this->testParser );
-               unset( $this->title );
-       }
-
-       public function testPreloadSimpleText() {
-               $this->assertPreloaded( 'simple', 'simple' );
-       }
-
-       public function testPreloadedPreIsUnstripped() {
-               $this->assertPreloaded(
-                       '<pre>monospaced</pre>',
-                       '<pre>monospaced</pre>',
-                       '<pre> in preloaded text must be unstripped (T29467)'
-               );
-       }
-
-       public function testPreloadedNowikiIsUnstripped() {
-               $this->assertPreloaded(
-                       '<nowiki>[[Dummy title]]</nowiki>',
-                       '<nowiki>[[Dummy title]]</nowiki>',
-                       '<nowiki> in preloaded text must be unstripped (T29467)'
-               );
-       }
-
-       protected function assertPreloaded( $expected, $text, $msg = '' ) {
-               $this->assertEquals(
-                       $expected,
-                       $this->testParser->getPreloadText(
-                               $text,
-                               $this->title,
-                               $this->testParserOptions
-                       ),
-                       $msg
-               );
-       }
-}
diff --git a/tests/phpunit/includes/parser/PreprocessorTest.php b/tests/phpunit/includes/parser/PreprocessorTest.php
deleted file mode 100644 (file)
index 6b3e05d..0000000
+++ /dev/null
@@ -1,296 +0,0 @@
-<?php
-
-use MediaWiki\MediaWikiServices;
-
-/**
- * @covers Preprocessor
- *
- * @covers Preprocessor_DOM
- * @covers PPDStack
- * @covers PPDStackElement
- * @covers PPDPart
- * @covers PPFrame_DOM
- * @covers PPTemplateFrame_DOM
- * @covers PPCustomFrame_DOM
- * @covers PPNode_DOM
- *
- * @covers Preprocessor_Hash
- * @covers PPDStack_Hash
- * @covers PPDStackElement_Hash
- * @covers PPDPart_Hash
- * @covers PPFrame_Hash
- * @covers PPTemplateFrame_Hash
- * @covers PPCustomFrame_Hash
- * @covers PPNode_Hash_Tree
- * @covers PPNode_Hash_Text
- * @covers PPNode_Hash_Array
- * @covers PPNode_Hash_Attr
- */
-class PreprocessorTest extends MediaWikiTestCase {
-       protected $mTitle = 'Page title';
-       protected $mPPNodeCount = 0;
-       /**
-        * @var ParserOptions
-        */
-       protected $mOptions;
-       /**
-        * @var array
-        */
-       protected $mPreprocessors;
-
-       protected static $classNames = [
-               Preprocessor_DOM::class,
-               Preprocessor_Hash::class
-       ];
-
-       protected function setUp() {
-               parent::setUp();
-               $this->mOptions = ParserOptions::newFromUserAndLang( new User,
-                       MediaWikiServices::getInstance()->getContentLanguage() );
-
-               $this->mPreprocessors = [];
-               foreach ( self::$classNames as $className ) {
-                       $this->mPreprocessors[$className] = new $className( $this );
-               }
-       }
-
-       function getStripList() {
-               return [ 'gallery', 'display map' /* Used by Maps, see r80025 CR */, '/foo' ];
-       }
-
-       protected static function addClassArg( $testCases ) {
-               $newTestCases = [];
-               foreach ( self::$classNames as $className ) {
-                       foreach ( $testCases as $testCase ) {
-                               array_unshift( $testCase, $className );
-                               $newTestCases[] = $testCase;
-                       }
-               }
-               return $newTestCases;
-       }
-
-       public static function provideCases() {
-               // phpcs:disable Generic.Files.LineLength
-               return self::addClassArg( [
-                       [ "Foo", "<root>Foo</root>" ],
-                       [ "<!-- Foo -->", "<root><comment>&lt;!-- Foo --&gt;</comment></root>" ],
-                       [ "<!-- Foo --><!-- Bar -->", "<root><comment>&lt;!-- Foo --&gt;</comment><comment>&lt;!-- Bar --&gt;</comment></root>" ],
-                       [ "<!-- Foo -->  <!-- Bar -->", "<root><comment>&lt;!-- Foo --&gt;</comment>  <comment>&lt;!-- Bar --&gt;</comment></root>" ],
-                       [ "<!-- Foo --> \n <!-- Bar -->", "<root><comment>&lt;!-- Foo --&gt;</comment> \n <comment>&lt;!-- Bar --&gt;</comment></root>" ],
-                       [ "<!-- Foo --> \n <!-- Bar -->\n", "<root><comment>&lt;!-- Foo --&gt;</comment> \n<comment> &lt;!-- Bar --&gt;\n</comment></root>" ],
-                       [ "<!-- Foo -->  <!-- Bar -->\n", "<root><comment>&lt;!-- Foo --&gt;</comment>  <comment>&lt;!-- Bar --&gt;</comment>\n</root>" ],
-                       [ "<!-->Bar", "<root><comment>&lt;!--&gt;Bar</comment></root>" ],
-                       [ "<!-- Comment -- comment", "<root><comment>&lt;!-- Comment -- comment</comment></root>" ],
-                       [ "== Foo ==\n  <!-- Bar -->\n== Baz ==\n", "<root><h level=\"2\" i=\"1\">== Foo ==</h>\n<comment>  &lt;!-- Bar --&gt;\n</comment><h level=\"2\" i=\"2\">== Baz ==</h>\n</root>" ],
-                       [ "<gallery/>", "<root><ext><name>gallery</name><attr></attr></ext></root>" ],
-                       [ "Foo <gallery/> Bar", "<root>Foo <ext><name>gallery</name><attr></attr></ext> Bar</root>" ],
-                       [ "<gallery></gallery>", "<root><ext><name>gallery</name><attr></attr><inner></inner><close>&lt;/gallery&gt;</close></ext></root>" ],
-                       [ "<foo> <gallery></gallery>", "<root>&lt;foo&gt; <ext><name>gallery</name><attr></attr><inner></inner><close>&lt;/gallery&gt;</close></ext></root>" ],
-                       [ "<foo> <gallery><gallery></gallery>", "<root>&lt;foo&gt; <ext><name>gallery</name><attr></attr><inner>&lt;gallery&gt;</inner><close>&lt;/gallery&gt;</close></ext></root>" ],
-                       [ "<noinclude> Foo bar </noinclude>", "<root><ignore>&lt;noinclude&gt;</ignore> Foo bar <ignore>&lt;/noinclude&gt;</ignore></root>" ],
-                       [ "<noinclude>\n{{Foo}}\n</noinclude>", "<root><ignore>&lt;noinclude&gt;</ignore>\n<template lineStart=\"1\"><title>Foo</title></template>\n<ignore>&lt;/noinclude&gt;</ignore></root>" ],
-                       [ "<noinclude>\n{{Foo}}\n</noinclude>\n", "<root><ignore>&lt;noinclude&gt;</ignore>\n<template lineStart=\"1\"><title>Foo</title></template>\n<ignore>&lt;/noinclude&gt;</ignore>\n</root>" ],
-                       [ "<gallery>foo bar", "<root>&lt;gallery&gt;foo bar</root>" ],
-                       [ "<{{foo}}>", "<root>&lt;<template><title>foo</title></template>&gt;</root>" ],
-                       [ "<{{{foo}}}>", "<root>&lt;<tplarg><title>foo</title></tplarg>&gt;</root>" ],
-                       [ "<gallery></gallery</gallery>", "<root><ext><name>gallery</name><attr></attr><inner>&lt;/gallery</inner><close>&lt;/gallery&gt;</close></ext></root>" ],
-                       [ "=== Foo === ", "<root><h level=\"3\" i=\"1\">=== Foo === </h></root>" ],
-                       [ "==<!-- -->= Foo === ", "<root><h level=\"2\" i=\"1\">==<comment>&lt;!-- --&gt;</comment>= Foo === </h></root>" ],
-                       [ "=== Foo ==<!-- -->= ", "<root><h level=\"1\" i=\"1\">=== Foo ==<comment>&lt;!-- --&gt;</comment>= </h></root>" ],
-                       [ "=== Foo ===<!-- -->\n", "<root><h level=\"3\" i=\"1\">=== Foo ===<comment>&lt;!-- --&gt;</comment></h>\n</root>" ],
-                       [ "=== Foo ===<!-- --> <!-- -->\n", "<root><h level=\"3\" i=\"1\">=== Foo ===<comment>&lt;!-- --&gt;</comment> <comment>&lt;!-- --&gt;</comment></h>\n</root>" ],
-                       [ "== Foo ==\n== Bar == \n", "<root><h level=\"2\" i=\"1\">== Foo ==</h>\n<h level=\"2\" i=\"2\">== Bar == </h>\n</root>" ],
-                       [ "===========", "<root><h level=\"5\" i=\"1\">===========</h></root>" ],
-                       [ "Foo\n=\n==\n=\n", "<root>Foo\n=\n==\n=\n</root>" ],
-                       [ "{{Foo}}", "<root><template><title>Foo</title></template></root>" ],
-                       [ "\n{{Foo}}", "<root>\n<template lineStart=\"1\"><title>Foo</title></template></root>" ],
-                       [ "{{Foo|bar}}", "<root><template><title>Foo</title><part><name index=\"1\" /><value>bar</value></part></template></root>" ],
-                       [ "{{Foo|bar}}a", "<root><template><title>Foo</title><part><name index=\"1\" /><value>bar</value></part></template>a</root>" ],
-                       [ "{{Foo|bar|baz}}", "<root><template><title>Foo</title><part><name index=\"1\" /><value>bar</value></part><part><name index=\"2\" /><value>baz</value></part></template></root>" ],
-                       [ "{{Foo|1=bar}}", "<root><template><title>Foo</title><part><name>1</name>=<value>bar</value></part></template></root>" ],
-                       [ "{{Foo|=bar}}", "<root><template><title>Foo</title><part><name></name>=<value>bar</value></part></template></root>" ],
-                       [ "{{Foo|bar=baz}}", "<root><template><title>Foo</title><part><name>bar</name>=<value>baz</value></part></template></root>" ],
-                       [ "{{Foo|{{bar}}=baz}}", "<root><template><title>Foo</title><part><name><template><title>bar</title></template></name>=<value>baz</value></part></template></root>" ],
-                       [ "{{Foo|1=bar|baz}}", "<root><template><title>Foo</title><part><name>1</name>=<value>bar</value></part><part><name index=\"1\" /><value>baz</value></part></template></root>" ],
-                       [ "{{Foo|1=bar|2=baz}}", "<root><template><title>Foo</title><part><name>1</name>=<value>bar</value></part><part><name>2</name>=<value>baz</value></part></template></root>" ],
-                       [ "{{Foo|bar|foo=baz}}", "<root><template><title>Foo</title><part><name index=\"1\" /><value>bar</value></part><part><name>foo</name>=<value>baz</value></part></template></root>" ],
-                       [ "{{{1}}}", "<root><tplarg><title>1</title></tplarg></root>" ],
-                       [ "{{{1|}}}", "<root><tplarg><title>1</title><part><name index=\"1\" /><value></value></part></tplarg></root>" ],
-                       [ "{{{Foo}}}", "<root><tplarg><title>Foo</title></tplarg></root>" ],
-                       [ "{{{Foo|}}}", "<root><tplarg><title>Foo</title><part><name index=\"1\" /><value></value></part></tplarg></root>" ],
-                       [ "{{{Foo|bar|baz}}}", "<root><tplarg><title>Foo</title><part><name index=\"1\" /><value>bar</value></part><part><name index=\"2\" /><value>baz</value></part></tplarg></root>" ],
-                       [ "{<!-- -->{Foo}}", "<root>{<comment>&lt;!-- --&gt;</comment>{Foo}}</root>" ],
-                       [ "{{{{Foobar}}}}", "<root>{<tplarg><title>Foobar</title></tplarg>}</root>" ],
-                       [ "{{{ {{Foo}} }}}", "<root><tplarg><title> <template><title>Foo</title></template> </title></tplarg></root>" ],
-                       [ "{{ {{{Foo}}} }}", "<root><template><title> <tplarg><title>Foo</title></tplarg> </title></template></root>" ],
-                       [ "{{{{{Foo}}}}}", "<root><template><title><tplarg><title>Foo</title></tplarg></title></template></root>" ],
-                       [ "{{{{{Foo}} }}}", "<root><tplarg><title><template><title>Foo</title></template> </title></tplarg></root>" ],
-                       [ "{{{{{{Foo}}}}}}", "<root><tplarg><title><tplarg><title>Foo</title></tplarg></title></tplarg></root>" ],
-                       [ "{{{{{{Foo}}}}}", "<root>{<template><title><tplarg><title>Foo</title></tplarg></title></template></root>" ],
-                       [ "[[[Foo]]", "<root>[[[Foo]]</root>" ],
-                       [ "{{Foo|[[[[bar]]|baz]]}}", "<root><template><title>Foo</title><part><name index=\"1\" /><value>[[[[bar]]|baz]]</value></part></template></root>" ], // This test is important, since it means the difference between having the [[ rule stacked or not
-                       [ "{{Foo|[[[[bar]|baz]]}}", "<root>{{Foo|[[[[bar]|baz]]}}</root>" ],
-                       [ "{{Foo|Foo [[[[bar]|baz]]}}", "<root>{{Foo|Foo [[[[bar]|baz]]}}</root>" ],
-                       [ "Foo <display map>Bar</display map             >Baz", "<root>Foo <ext><name>display map</name><attr></attr><inner>Bar</inner><close>&lt;/display map             &gt;</close></ext>Baz</root>" ],
-                       [ "Foo <display map foo>Bar</display map             >Baz", "<root>Foo <ext><name>display map</name><attr> foo</attr><inner>Bar</inner><close>&lt;/display map             &gt;</close></ext>Baz</root>" ],
-                       [ "Foo <gallery bar=\"baz\" />", "<root>Foo <ext><name>gallery</name><attr> bar=&quot;baz&quot; </attr></ext></root>" ],
-                       [ "Foo <gallery bar=\"1\" baz=2 />", "<root>Foo <ext><name>gallery</name><attr> bar=&quot;1&quot; baz=2 </attr></ext></root>" ],
-                       [ "</foo>Foo<//foo>", "<root><ext><name>/foo</name><attr></attr><inner>Foo</inner><close>&lt;//foo&gt;</close></ext></root>" ], # Worth blacklisting IMHO
-                       [ "{{#ifexpr: ({{{1|1}}} = 2) | Foo | Bar }}", "<root><template><title>#ifexpr: (<tplarg><title>1</title><part><name index=\"1\" /><value>1</value></part></tplarg> = 2) </title><part><name index=\"1\" /><value> Foo </value></part><part><name index=\"2\" /><value> Bar </value></part></template></root>" ],
-                       [ "{{#if: {{{1|}}} | Foo | {{Bar}} }}", "<root><template><title>#if: <tplarg><title>1</title><part><name index=\"1\" /><value></value></part></tplarg> </title><part><name index=\"1\" /><value> Foo </value></part><part><name index=\"2\" /><value> <template><title>Bar</title></template> </value></part></template></root>" ],
-                       [ "{{#if: {{{1|}}} | Foo | [[Bar]] }}", "<root><template><title>#if: <tplarg><title>1</title><part><name index=\"1\" /><value></value></part></tplarg> </title><part><name index=\"1\" /><value> Foo </value></part><part><name index=\"2\" /><value> [[Bar]] </value></part></template></root>" ],
-                       [ "{{#if: {{{1|}}} | [[Foo]] | Bar }}", "<root><template><title>#if: <tplarg><title>1</title><part><name index=\"1\" /><value></value></part></tplarg> </title><part><name index=\"1\" /><value> [[Foo]] </value></part><part><name index=\"2\" /><value> Bar </value></part></template></root>" ],
-                       [ "{{#if: {{{1|}}} | 1 | {{#if: {{{1|}}} | 2 | 3 }} }}", "<root><template><title>#if: <tplarg><title>1</title><part><name index=\"1\" /><value></value></part></tplarg> </title><part><name index=\"1\" /><value> 1 </value></part><part><name index=\"2\" /><value> <template><title>#if: <tplarg><title>1</title><part><name index=\"1\" /><value></value></part></tplarg> </title><part><name index=\"1\" /><value> 2 </value></part><part><name index=\"2\" /><value> 3 </value></part></template> </value></part></template></root>" ],
-                       [ "{{ {{Foo}}", "<root>{{ <template><title>Foo</title></template></root>" ],
-                       [ "{{Foobar {{Foo}} {{Bar}} {{Baz}} ", "<root>{{Foobar <template><title>Foo</title></template> <template><title>Bar</title></template> <template><title>Baz</title></template> </root>" ],
-                       [ "[[Foo]] |", "<root>[[Foo]] |</root>" ],
-                       [ "{{Foo|Bar|", "<root>{{Foo|Bar|</root>" ],
-                       [ "[[Foo]", "<root>[[Foo]</root>" ],
-                       [ "[[Foo|Bar]", "<root>[[Foo|Bar]</root>" ],
-                       [ "{{Foo| [[Bar] }}", "<root>{{Foo| [[Bar] }}</root>" ],
-                       [ "{{Foo| [[Bar|Baz] }}", "<root>{{Foo| [[Bar|Baz] }}</root>" ],
-                       [ "{{Foo|bar=[[baz]}}", "<root>{{Foo|bar=[[baz]}}</root>" ],
-                       [ "{{foo|", "<root>{{foo|</root>" ],
-                       [ "{{foo|}", "<root>{{foo|}</root>" ],
-                       [ "{{foo|} }}", "<root><template><title>foo</title><part><name index=\"1\" /><value>} </value></part></template></root>" ],
-                       [ "{{foo|bar=|}", "<root>{{foo|bar=|}</root>" ],
-                       [ "{{Foo|} Bar=", "<root>{{Foo|} Bar=</root>" ],
-                       [ "{{Foo|} Bar=}}", "<root><template><title>Foo</title><part><name>} Bar</name>=<value></value></part></template></root>" ],
-                       /* [ file_get_contents( __DIR__ . '/QuoteQuran.txt' ], file_get_contents( __DIR__ . '/QuoteQuranExpanded.txt' ) ], */
-               ] );
-               // phpcs:enable
-       }
-
-       /**
-        * Get XML preprocessor tree from the preprocessor (which may not be the
-        * native XML-based one).
-        *
-        * @param string $className
-        * @param string $wikiText
-        * @return string
-        */
-       protected function preprocessToXml( $className, $wikiText ) {
-               $preprocessor = $this->mPreprocessors[$className];
-               if ( method_exists( $preprocessor, 'preprocessToXml' ) ) {
-                       return $this->normalizeXml( $preprocessor->preprocessToXml( $wikiText ) );
-               }
-
-               $dom = $preprocessor->preprocessToObj( $wikiText );
-               if ( is_callable( [ $dom, 'saveXML' ] ) ) {
-                       return $dom->saveXML();
-               } else {
-                       return $this->normalizeXml( $dom->__toString() );
-               }
-       }
-
-       /**
-        * Normalize XML string to the form that a DOMDocument saves out.
-        *
-        * @param string $xml
-        * @return string
-        */
-       protected function normalizeXml( $xml ) {
-               // Normalize self-closing tags
-               $xml = preg_replace( '!<([a-z]+)/>!', '<$1></$1>', str_replace( ' />', '/>', $xml ) );
-               // Remove <equals> tags, which only occur in Preprocessor_Hash and
-               // have no semantic value
-               $xml = preg_replace( '!</?equals>!', '', $xml );
-               return $xml;
-       }
-
-       /**
-        * @dataProvider provideCases
-        */
-       public function testPreprocessorOutput( $className, $wikiText, $expectedXml ) {
-               $this->assertEquals( $this->normalizeXml( $expectedXml ),
-                       $this->preprocessToXml( $className, $wikiText ) );
-       }
-
-       /**
-        * These are more complex test cases taken out of wiki articles.
-        */
-       public static function provideFiles() {
-               // phpcs:disable Generic.Files.LineLength
-               return self::addClassArg( [
-                       [ "QuoteQuran" ], # https://en.wikipedia.org/w/index.php?title=Template:QuoteQuran/sandbox&oldid=237348988 GFDL + CC BY-SA by Striver
-                       [ "Factorial" ], # https://en.wikipedia.org/w/index.php?title=Template:Factorial&oldid=98548758 GFDL + CC BY-SA by Polonium
-                       [ "All_system_messages" ], # https://tl.wiktionary.org/w/index.php?title=Suleras:All_system_messages&oldid=2765 GPL text generated by MediaWiki
-                       [ "Fundraising" ], # https://tl.wiktionary.org/w/index.php?title=MediaWiki:Sitenotice&oldid=5716 GFDL + CC BY-SA, copied there by Sky Harbor.
-                       [ "NestedTemplates" ], # T29936
-               ] );
-               // phpcs:enable
-       }
-
-       /**
-        * @dataProvider provideFiles
-        */
-       public function testPreprocessorOutputFiles( $className, $filename ) {
-               $folder = __DIR__ . "/../../../parser/preprocess";
-               $wikiText = file_get_contents( "$folder/$filename.txt" );
-               $output = $this->preprocessToXml( $className, $wikiText );
-
-               $expectedFilename = "$folder/$filename.expected";
-               if ( file_exists( $expectedFilename ) ) {
-                       $expectedXml = $this->normalizeXml( file_get_contents( $expectedFilename ) );
-                       $this->assertEquals( $expectedXml, $output );
-               } else {
-                       $tempFilename = tempnam( $folder, "$filename." );
-                       file_put_contents( $tempFilename, $output );
-                       $this->markTestIncomplete( "File $expectedFilename missing. Output stored as $tempFilename" );
-               }
-       }
-
-       /**
-        * Tests from T30642 · https://phabricator.wikimedia.org/T30642
-        */
-       public static function provideHeadings() {
-               // phpcs:disable Generic.Files.LineLength
-               return self::addClassArg( [
-                       /* These should become headings: */
-                       [ "== h ==<!--c1-->", "<root><h level=\"2\" i=\"1\">== h ==<comment>&lt;!--c1--&gt;</comment></h></root>" ],
-                       [ "== h ==      <!--c1-->", "<root><h level=\"2\" i=\"1\">== h ==       <comment>&lt;!--c1--&gt;</comment></h></root>" ],
-                       [ "== h ==<!--c1-->     ", "<root><h level=\"2\" i=\"1\">== h ==<comment>&lt;!--c1--&gt;</comment>      </h></root>" ],
-                       [ "== h ==      <!--c1-->       ", "<root><h level=\"2\" i=\"1\">== h ==        <comment>&lt;!--c1--&gt;</comment>      </h></root>" ],
-                       [ "== h ==<!--c1--><!--c2-->", "<root><h level=\"2\" i=\"1\">== h ==<comment>&lt;!--c1--&gt;</comment><comment>&lt;!--c2--&gt;</comment></h></root>" ],
-                       [ "== h ==      <!--c1--><!--c2-->", "<root><h level=\"2\" i=\"1\">== h ==      <comment>&lt;!--c1--&gt;</comment><comment>&lt;!--c2--&gt;</comment></h></root>" ],
-                       [ "== h ==<!--c1--><!--c2-->    ", "<root><h level=\"2\" i=\"1\">== h ==<comment>&lt;!--c1--&gt;</comment><comment>&lt;!--c2--&gt;</comment>    </h></root>" ],
-                       [ "== h ==      <!--c1--><!--c2-->      ", "<root><h level=\"2\" i=\"1\">== h ==        <comment>&lt;!--c1--&gt;</comment><comment>&lt;!--c2--&gt;</comment>    </h></root>" ],
-                       [ "== h ==      <!--c1-->  <!--c2-->", "<root><h level=\"2\" i=\"1\">== h ==    <comment>&lt;!--c1--&gt;</comment>  <comment>&lt;!--c2--&gt;</comment></h></root>" ],
-                       [ "== h ==<!--c1-->  <!--c2-->  ", "<root><h level=\"2\" i=\"1\">== h ==<comment>&lt;!--c1--&gt;</comment>  <comment>&lt;!--c2--&gt;</comment>  </h></root>" ],
-                       [ "== h ==      <!--c1-->  <!--c2-->    ", "<root><h level=\"2\" i=\"1\">== h ==        <comment>&lt;!--c1--&gt;</comment>  <comment>&lt;!--c2--&gt;</comment>  </h></root>" ],
-                       [ "== h ==<!--c1--><!--c2--><!--c3-->", "<root><h level=\"2\" i=\"1\">== h ==<comment>&lt;!--c1--&gt;</comment><comment>&lt;!--c2--&gt;</comment><comment>&lt;!--c3--&gt;</comment></h></root>" ],
-                       [ "== h ==<!--c1-->  <!--c2--><!--c3-->", "<root><h level=\"2\" i=\"1\">== h ==<comment>&lt;!--c1--&gt;</comment>  <comment>&lt;!--c2--&gt;</comment><comment>&lt;!--c3--&gt;</comment></h></root>" ],
-                       [ "== h ==<!--c1--><!--c2-->  <!--c3-->", "<root><h level=\"2\" i=\"1\">== h ==<comment>&lt;!--c1--&gt;</comment><comment>&lt;!--c2--&gt;</comment>  <comment>&lt;!--c3--&gt;</comment></h></root>" ],
-                       [ "== h ==<!--c1-->  <!--c2-->  <!--c3-->", "<root><h level=\"2\" i=\"1\">== h ==<comment>&lt;!--c1--&gt;</comment>  <comment>&lt;!--c2--&gt;</comment>  <comment>&lt;!--c3--&gt;</comment></h></root>" ],
-                       [ "== h ==  <!--c1--><!--c2--><!--c3-->", "<root><h level=\"2\" i=\"1\">== h ==  <comment>&lt;!--c1--&gt;</comment><comment>&lt;!--c2--&gt;</comment><comment>&lt;!--c3--&gt;</comment></h></root>" ],
-                       [ "== h ==  <!--c1-->  <!--c2--><!--c3-->", "<root><h level=\"2\" i=\"1\">== h ==  <comment>&lt;!--c1--&gt;</comment>  <comment>&lt;!--c2--&gt;</comment><comment>&lt;!--c3--&gt;</comment></h></root>" ],
-                       [ "== h ==  <!--c1--><!--c2-->  <!--c3-->", "<root><h level=\"2\" i=\"1\">== h ==  <comment>&lt;!--c1--&gt;</comment><comment>&lt;!--c2--&gt;</comment>  <comment>&lt;!--c3--&gt;</comment></h></root>" ],
-                       [ "== h ==  <!--c1-->  <!--c2-->  <!--c3-->", "<root><h level=\"2\" i=\"1\">== h ==  <comment>&lt;!--c1--&gt;</comment>  <comment>&lt;!--c2--&gt;</comment>  <comment>&lt;!--c3--&gt;</comment></h></root>" ],
-                       [ "== h ==<!--c1--><!--c2--><!--c3-->  ", "<root><h level=\"2\" i=\"1\">== h ==<comment>&lt;!--c1--&gt;</comment><comment>&lt;!--c2--&gt;</comment><comment>&lt;!--c3--&gt;</comment>  </h></root>" ],
-                       [ "== h ==<!--c1-->  <!--c2--><!--c3-->  ", "<root><h level=\"2\" i=\"1\">== h ==<comment>&lt;!--c1--&gt;</comment>  <comment>&lt;!--c2--&gt;</comment><comment>&lt;!--c3--&gt;</comment>  </h></root>" ],
-                       [ "== h ==<!--c1--><!--c2-->  <!--c3-->  ", "<root><h level=\"2\" i=\"1\">== h ==<comment>&lt;!--c1--&gt;</comment><comment>&lt;!--c2--&gt;</comment>  <comment>&lt;!--c3--&gt;</comment>  </h></root>" ],
-                       [ "== h ==<!--c1-->  <!--c2-->  <!--c3-->  ", "<root><h level=\"2\" i=\"1\">== h ==<comment>&lt;!--c1--&gt;</comment>  <comment>&lt;!--c2--&gt;</comment>  <comment>&lt;!--c3--&gt;</comment>  </h></root>" ],
-                       [ "== h ==  <!--c1--><!--c2--><!--c3-->  ", "<root><h level=\"2\" i=\"1\">== h ==  <comment>&lt;!--c1--&gt;</comment><comment>&lt;!--c2--&gt;</comment><comment>&lt;!--c3--&gt;</comment>  </h></root>" ],
-                       [ "== h ==  <!--c1-->  <!--c2--><!--c3-->  ", "<root><h level=\"2\" i=\"1\">== h ==  <comment>&lt;!--c1--&gt;</comment>  <comment>&lt;!--c2--&gt;</comment><comment>&lt;!--c3--&gt;</comment>  </h></root>" ],
-                       [ "== h ==  <!--c1--><!--c2-->  <!--c3-->  ", "<root><h level=\"2\" i=\"1\">== h ==  <comment>&lt;!--c1--&gt;</comment><comment>&lt;!--c2--&gt;</comment>  <comment>&lt;!--c3--&gt;</comment>  </h></root>" ],
-                       [ "== h ==  <!--c1-->  <!--c2-->  <!--c3-->  ", "<root><h level=\"2\" i=\"1\">== h ==  <comment>&lt;!--c1--&gt;</comment>  <comment>&lt;!--c2--&gt;</comment>  <comment>&lt;!--c3--&gt;</comment>  </h></root>" ],
-                       [ "== h ==<!--c1-->     <!--c2-->", "<root><h level=\"2\" i=\"1\">== h ==<comment>&lt;!--c1--&gt;</comment>     <comment>&lt;!--c2--&gt;</comment></h></root>" ],
-                       [ "== h ==      <!--c1-->       <!--c2-->", "<root><h level=\"2\" i=\"1\">== h ==       <comment>&lt;!--c1--&gt;</comment>      <comment>&lt;!--c2--&gt;</comment></h></root>" ],
-                       [ "== h ==<!--c1-->     <!--c2-->       ", "<root><h level=\"2\" i=\"1\">== h ==<comment>&lt;!--c1--&gt;</comment>      <comment>&lt;!--c2--&gt;</comment>      </h></root>" ],
-
-                       /* These are not working: */
-                       [ "== h == x <!--c1--><!--c2--><!--c3-->  ", "<root>== h == x <comment>&lt;!--c1--&gt;</comment><comment>&lt;!--c2--&gt;</comment><comment>&lt;!--c3--&gt;</comment>  </root>" ],
-                       [ "== h ==<!--c1--> x <!--c2--><!--c3-->  ", "<root>== h ==<comment>&lt;!--c1--&gt;</comment> x <comment>&lt;!--c2--&gt;</comment><comment>&lt;!--c3--&gt;</comment>  </root>" ],
-                       [ "== h ==<!--c1--><!--c2--><!--c3--> x ", "<root>== h ==<comment>&lt;!--c1--&gt;</comment><comment>&lt;!--c2--&gt;</comment><comment>&lt;!--c3--&gt;</comment> x </root>" ],
-               ] );
-               // phpcs:enable
-       }
-
-       /**
-        * @dataProvider provideHeadings
-        */
-       public function testHeadings( $className, $wikiText, $expectedXml ) {
-               $this->assertEquals( $this->normalizeXml( $expectedXml ),
-                       $this->preprocessToXml( $className, $wikiText ) );
-       }
-}
diff --git a/tests/phpunit/includes/parser/TidyTest.php b/tests/phpunit/includes/parser/TidyTest.php
deleted file mode 100644 (file)
index 898ef2d..0000000
+++ /dev/null
@@ -1,64 +0,0 @@
-<?php
-
-/**
- * @group Parser
- * @covers MWTidy
- */
-class TidyTest extends MediaWikiTestCase {
-
-       protected function setUp() {
-               parent::setUp();
-               if ( !MWTidy::isEnabled() ) {
-                       $this->markTestSkipped( 'Tidy not found' );
-               }
-       }
-
-       /**
-        * @dataProvider provideTestWrapping
-        */
-       public function testTidyWrapping( $expected, $text, $msg = '' ) {
-               $text = MWTidy::tidy( $text );
-               // We don't care about where Tidy wants to stick is <p>s
-               $text = trim( preg_replace( '#</?p>#', '', $text ) );
-               // Windows, we love you!
-               $text = str_replace( "\r", '', $text );
-               $this->assertEquals( $expected, $text, $msg );
-       }
-
-       public static function provideTestWrapping() {
-               $testMathML = <<<'MathML'
-<math xmlns="http://www.w3.org/1998/Math/MathML">
-    <mrow>
-      <mi>a</mi>
-      <mo>&InvisibleTimes;</mo>
-      <msup>
-        <mi>x</mi>
-        <mn>2</mn>
-      </msup>
-      <mo>+</mo>
-      <mi>b</mi>
-      <mo>&InvisibleTimes; </mo>
-      <mi>x</mi>
-      <mo>+</mo>
-      <mi>c</mi>
-    </mrow>
-  </math>
-MathML;
-               return [
-                       [
-                               '<mw:editsection page="foo" section="bar">foo</mw:editsection>',
-                               '<mw:editsection page="foo" section="bar">foo</mw:editsection>',
-                               '<mw:editsection> should survive tidy'
-                       ],
-                       [
-                               '<editsection page="foo" section="bar">foo</editsection>',
-                               '<editsection page="foo" section="bar">foo</editsection>',
-                               '<editsection> should survive tidy'
-                       ],
-                       [ '<mw:toc>foo</mw:toc>', '<mw:toc>foo</mw:toc>', '<mw:toc> should survive tidy' ],
-                       [ "<link foo=\"bar\" />foo", '<link foo="bar"/>foo', '<link> should survive tidy' ],
-                       [ "<meta foo=\"bar\" />foo", '<meta foo="bar"/>foo', '<meta> should survive tidy' ],
-                       [ $testMathML, $testMathML, '<math> should survive tidy' ],
-               ];
-       }
-}
diff --git a/tests/phpunit/includes/password/PasswordFactoryTest.php b/tests/phpunit/includes/password/PasswordFactoryTest.php
deleted file mode 100644 (file)
index a7b3557..0000000
+++ /dev/null
@@ -1,124 +0,0 @@
-<?php
-
-/**
- * @covers PasswordFactory
- */
-class PasswordFactoryTest extends MediaWikiTestCase {
-       public function testConstruct() {
-               $pf = new PasswordFactory();
-               $this->assertEquals( [ '' ], array_keys( $pf->getTypes() ) );
-               $this->assertEquals( '', $pf->getDefaultType() );
-
-               $pf = new PasswordFactory( [
-                       'foo' => [ 'class' => 'FooPassword' ],
-                       'bar' => [ 'class' => 'BarPassword', 'baz' => 'boom' ],
-               ], 'foo' );
-               $this->assertEquals( [ '', 'foo', 'bar' ], array_keys( $pf->getTypes() ) );
-               $this->assertArraySubset( [ 'class' => 'BarPassword', 'baz' => 'boom' ], $pf->getTypes()['bar'] );
-               $this->assertEquals( 'foo', $pf->getDefaultType() );
-       }
-
-       public function testRegister() {
-               $pf = new PasswordFactory;
-               $pf->register( 'foo', [ 'class' => InvalidPassword::class ] );
-               $this->assertArrayHasKey( 'foo', $pf->getTypes() );
-       }
-
-       public function testSetDefaultType() {
-               $pf = new PasswordFactory;
-               $pf->register( '1', [ 'class' => InvalidPassword::class ] );
-               $pf->register( '2', [ 'class' => InvalidPassword::class ] );
-               $pf->setDefaultType( '1' );
-               $this->assertSame( '1', $pf->getDefaultType() );
-               $pf->setDefaultType( '2' );
-               $this->assertSame( '2', $pf->getDefaultType() );
-       }
-
-       /**
-        * @expectedException Exception
-        */
-       public function testSetDefaultTypeError() {
-               $pf = new PasswordFactory;
-               $pf->setDefaultType( 'bogus' );
-       }
-
-       public function testInit() {
-               $config = new HashConfig( [
-                       'PasswordConfig' => [
-                               'foo' => [ 'class' => InvalidPassword::class ],
-                       ],
-                       'PasswordDefault' => 'foo'
-               ] );
-               $pf = new PasswordFactory;
-               $pf->init( $config );
-               $this->assertSame( 'foo', $pf->getDefaultType() );
-               $this->assertArrayHasKey( 'foo', $pf->getTypes() );
-       }
-
-       public function testNewFromCiphertext() {
-               $pf = new PasswordFactory;
-               $pf->register( 'B', [ 'class' => MWSaltedPassword::class ] );
-               $pw = $pf->newFromCiphertext( ':B:salt:d529e941509eb9e9b9cfaeae1fe7ca23' );
-               $this->assertInstanceOf( MWSaltedPassword::class, $pw );
-       }
-
-       public function provideNewFromCiphertextErrors() {
-               return [ [ 'blah' ], [ ':blah:' ] ];
-       }
-
-       /**
-        * @dataProvider provideNewFromCiphertextErrors
-        * @expectedException PasswordError
-        */
-       public function testNewFromCiphertextErrors( $hash ) {
-               $pf = new PasswordFactory;
-               $pf->register( 'B', [ 'class' => MWSaltedPassword::class ] );
-               $pf->newFromCiphertext( $hash );
-       }
-
-       public function testNewFromType() {
-               $pf = new PasswordFactory;
-               $pf->register( 'B', [ 'class' => MWSaltedPassword::class ] );
-               $pw = $pf->newFromType( 'B' );
-               $this->assertInstanceOf( MWSaltedPassword::class, $pw );
-       }
-
-       /**
-        * @expectedException PasswordError
-        */
-       public function testNewFromTypeError() {
-               $pf = new PasswordFactory;
-               $pf->register( 'B', [ 'class' => MWSaltedPassword::class ] );
-               $pf->newFromType( 'bogus' );
-       }
-
-       public function testNewFromPlaintext() {
-               $pf = new PasswordFactory;
-               $pf->register( 'A', [ 'class' => MWOldPassword::class ] );
-               $pf->register( 'B', [ 'class' => MWSaltedPassword::class ] );
-               $pf->setDefaultType( 'A' );
-
-               $this->assertInstanceOf( InvalidPassword::class, $pf->newFromPlaintext( null ) );
-               $this->assertInstanceOf( MWOldPassword::class, $pf->newFromPlaintext( 'password' ) );
-               $this->assertInstanceOf( MWSaltedPassword::class,
-                       $pf->newFromPlaintext( 'password', $pf->newFromType( 'B' ) ) );
-       }
-
-       public function testNeedsUpdate() {
-               $pf = new PasswordFactory;
-               $pf->register( 'A', [ 'class' => MWOldPassword::class ] );
-               $pf->register( 'B', [ 'class' => MWSaltedPassword::class ] );
-               $pf->setDefaultType( 'A' );
-
-               $this->assertFalse( $pf->needsUpdate( $pf->newFromType( 'A' ) ) );
-               $this->assertTrue( $pf->needsUpdate( $pf->newFromType( 'B' ) ) );
-       }
-
-       public function testGenerateRandomPasswordString() {
-               $this->assertSame( 13, strlen( PasswordFactory::generateRandomPasswordString( 13 ) ) );
-       }
-
-       public function testNewInvalidPassword() {
-               $this->assertInstanceOf( InvalidPassword::class, PasswordFactory::newInvalidPassword() );
-       }
-}
diff --git a/tests/phpunit/includes/password/PasswordTest.php b/tests/phpunit/includes/password/PasswordTest.php
deleted file mode 100644 (file)
index 61a5147..0000000
+++ /dev/null
@@ -1,33 +0,0 @@
-<?php
-/**
- * Testing framework for the Password infrastructure
- *
- * 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; either version 2 of the License, or
- * (at your option) any later version.
- *
- * 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.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- */
-
-/**
- * @covers InvalidPassword
- */
-class PasswordTest extends MediaWikiTestCase {
-       public function testInvalidPlaintext() {
-               $passwordFactory = new PasswordFactory();
-               $invalid = $passwordFactory->newFromPlaintext( null );
-
-               $this->assertInstanceOf( InvalidPassword::class, $invalid );
-       }
-}
diff --git a/tests/phpunit/includes/preferences/FiltersTest.php b/tests/phpunit/includes/preferences/FiltersTest.php
deleted file mode 100644 (file)
index 60b01b8..0000000
+++ /dev/null
@@ -1,141 +0,0 @@
-<?php
-/**
- * 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; either version 2 of the License, or
- * (at your option) any later version.
- *
- * 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.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- */
-
-use MediaWiki\Preferences\IntvalFilter;
-use MediaWiki\Preferences\MultiUsernameFilter;
-use MediaWiki\Preferences\TimezoneFilter;
-
-/**
- * @group Preferences
- */
-class FiltersTest extends MediaWikiTestCase {
-       /**
-        * @covers MediaWiki\Preferences\IntvalFilter::filterFromForm()
-        * @covers MediaWiki\Preferences\IntvalFilter::filterForForm()
-        */
-       public function testIntvalFilter() {
-               $filter = new IntvalFilter();
-               self::assertSame( 0, $filter->filterFromForm( '0' ) );
-               self::assertSame( 3, $filter->filterFromForm( '3' ) );
-               self::assertSame( '123', $filter->filterForForm( '123' ) );
-       }
-
-       /**
-        * @covers       MediaWiki\Preferences\TimezoneFilter::filterFromForm()
-        * @dataProvider provideTimezoneFilter
-        *
-        * @param string $input
-        * @param string $expected
-        */
-       public function testTimezoneFilter( $input, $expected ) {
-               $filter = new TimezoneFilter();
-               $result = $filter->filterFromForm( $input );
-               self::assertEquals( $expected, $result );
-       }
-
-       public function provideTimezoneFilter() {
-               return [
-                       [ 'ZoneInfo', 'Offset|0' ],
-                       [ 'ZoneInfo|bogus', 'Offset|0' ],
-                       [ 'System', 'System' ],
-                       [ '2:30', 'Offset|150' ],
-               ];
-       }
-
-       /**
-        * @covers MediaWiki\Preferences\MultiUsernameFilter::filterFromForm()
-        * @dataProvider provideMultiUsernameFilterFrom
-        *
-        * @param string $input
-        * @param string|null $expected
-        */
-       public function testMultiUsernameFilterFrom( $input, $expected ) {
-               $filter = $this->makeMultiUsernameFilter();
-               $result = $filter->filterFromForm( $input );
-               self::assertSame( $expected, $result );
-       }
-
-       public function provideMultiUsernameFilterFrom() {
-               return [
-                       [ '', null ],
-                       [ "\n\n\n", null ],
-                       [ 'Foo', '1' ],
-                       [ "\n\n\nFoo\nBar\n", "1\n2" ],
-                       [ "Baz\nInvalid\nFoo", "3\n1" ],
-                       [ "Invalid", null ],
-                       [ "Invalid\n\n\nInvalid\n", null ],
-               ];
-       }
-
-       /**
-        * @covers MediaWiki\Preferences\MultiUsernameFilter::filterForForm()
-        * @dataProvider provideMultiUsernameFilterFor
-        *
-        * @param string $input
-        * @param string $expected
-        */
-       public function testMultiUsernameFilterFor( $input, $expected ) {
-               $filter = $this->makeMultiUsernameFilter();
-               $result = $filter->filterForForm( $input );
-               self::assertSame( $expected, $result );
-       }
-
-       public function provideMultiUsernameFilterFor() {
-               return [
-                       [ '', '' ],
-                       [ "\n", '' ],
-                       [ '1', 'Foo' ],
-                       [ "\n1\n\n2\377\n", "Foo\nBar" ],
-                       [ "666\n667", '' ],
-               ];
-       }
-
-       private function makeMultiUsernameFilter() {
-               $userMapping = [
-                       'Foo' => 1,
-                       'Bar' => 2,
-                       'Baz' => 3,
-               ];
-               $flipped = array_flip( $userMapping );
-               $idLookup = self::getMockBuilder( CentralIdLookup::class )
-                       ->disableOriginalConstructor()
-                       ->setMethods( [ 'centralIdsFromNames', 'namesFromCentralIds' ] )
-                       ->getMockForAbstractClass();
-
-               $idLookup->method( 'centralIdsFromNames' )
-                       ->will( self::returnCallback( function ( $names ) use ( $userMapping ) {
-                               $ids = [];
-                               foreach ( $names as $name ) {
-                                       $ids[] = $userMapping[$name] ?? null;
-                               }
-                               return array_filter( $ids, 'is_numeric' );
-                       } ) );
-               $idLookup->method( 'namesFromCentralIds' )
-                       ->will( self::returnCallback( function ( $ids ) use ( $flipped ) {
-                               $names = [];
-                               foreach ( $ids as $id ) {
-                                       $names[] = $flipped[$id] ?? null;
-                               }
-                               return array_filter( $names, 'is_string' );
-                       } ) );
-
-               return new MultiUsernameFilter( $idLookup );
-       }
-}
diff --git a/tests/phpunit/includes/registration/ExtensionJsonValidatorTest.php b/tests/phpunit/includes/registration/ExtensionJsonValidatorTest.php
deleted file mode 100644 (file)
index 46c697f..0000000
+++ /dev/null
@@ -1,97 +0,0 @@
-<?php
-/**
- * Copyright (C) 2018 Kunal Mehta <legoktm@member.fsf.org>
- *
- * 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; either version 2 of the License, or
- * (at your option) any later version.
- *
- * 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.
- *
- */
-
-/**
- * @covers ExtensionJsonValidator
- */
-class ExtensionJsonValidatorTest extends MediaWikiTestCase {
-
-       /**
-        * @dataProvider provideValidate
-        */
-       public function testValidate( $file, $expected ) {
-               // If a dependency is missing, skip this test.
-               $validator = new ExtensionJsonValidator( function ( $msg ) {
-                       $this->markTestSkipped( $msg );
-               } );
-
-               if ( is_string( $expected ) ) {
-                       $this->setExpectedException(
-                               ExtensionJsonValidationError::class,
-                               $expected
-                       );
-               }
-
-               $dir = __DIR__ . '/../../data/registration/';
-               $this->assertSame(
-                       $expected,
-                       $validator->validate( $dir . $file )
-               );
-       }
-
-       public function provideValidate() {
-               return [
-                       [
-                               'notjson.txt',
-                               'notjson.txt is not valid JSON'
-                       ],
-                       [
-                               'duplicate_keys.json',
-                               'Duplicate key: name'
-                       ],
-                       [
-                               'no_manifest_version.json',
-                               'no_manifest_version.json does not have manifest_version set.'
-                       ],
-                       [
-                               'old_manifest_version.json',
-                               'old_manifest_version.json is using a non-supported schema version'
-                       ],
-                       [
-                               'newer_manifest_version.json',
-                               'newer_manifest_version.json is using a non-supported schema version'
-                       ],
-                       [
-                               'bad_spdx.json',
-                               "bad_spdx.json did not pass validation.
-[license-name] Invalid SPDX license identifier, see <https://spdx.org/licenses/>"
-                       ],
-                       [
-                               'invalid.json',
-                               "invalid.json did not pass validation.
-[license-name] Array value found, but a string is required"
-                       ],
-                       [
-                               'good.json',
-                               true
-                       ],
-                       [
-                               'bad_url.json', 'bad_url.json did not pass validation.
-[url] Should use HTTPS for www.mediawiki.org URLs'
-                       ],
-                       [
-                               'bad_url2.json', 'bad_url2.json did not pass validation.
-[url] Should use www.mediawiki.org domain
-[url] Should use HTTPS for www.mediawiki.org URLs'
-                       ]
-               ];
-       }
-
-}
diff --git a/tests/phpunit/includes/registration/ExtensionProcessorTest.php b/tests/phpunit/includes/registration/ExtensionProcessorTest.php
deleted file mode 100644 (file)
index cdd5c63..0000000
+++ /dev/null
@@ -1,829 +0,0 @@
-<?php
-
-use Wikimedia\TestingAccessWrapper;
-
-/**
- * @covers ExtensionProcessor
- */
-class ExtensionProcessorTest extends MediaWikiTestCase {
-
-       private $dir, $dirname;
-
-       public function setUp() {
-               parent::setUp();
-               $this->dir = __DIR__ . '/FooBar/extension.json';
-               $this->dirname = dirname( $this->dir );
-       }
-
-       /**
-        * 'name' is absolutely required
-        *
-        * @var array
-        */
-       public static $default = [
-               'name' => 'FooBar',
-       ];
-
-       public function testExtractInfo() {
-               // Test that attributes that begin with @ are ignored
-               $processor = new ExtensionProcessor();
-               $processor->extractInfo( $this->dir, self::$default + [
-                       '@metadata' => [ 'foobarbaz' ],
-                       'AnAttribute' => [ 'omg' ],
-                       'AutoloadClasses' => [ 'FooBar' => 'includes/FooBar.php' ],
-                       'SpecialPages' => [ 'Foo' => 'SpecialFoo' ],
-                       'callback' => 'FooBar::onRegistration',
-               ], 1 );
-
-               $extracted = $processor->getExtractedInfo();
-               $attributes = $extracted['attributes'];
-               $this->assertArrayHasKey( 'AnAttribute', $attributes );
-               $this->assertArrayNotHasKey( '@metadata', $attributes );
-               $this->assertArrayNotHasKey( 'AutoloadClasses', $attributes );
-               $this->assertSame(
-                       [ 'FooBar' => 'FooBar::onRegistration' ],
-                       $extracted['callbacks']
-               );
-               $this->assertSame(
-                       [ 'Foo' => 'SpecialFoo' ],
-                       $extracted['globals']['wgSpecialPages']
-               );
-       }
-
-       public function testExtractNamespaces() {
-               // Test that namespace IDs can be overwritten
-               if ( !defined( 'MW_EXTENSION_PROCESSOR_TEST_EXTRACT_INFO_X' ) ) {
-                       define( 'MW_EXTENSION_PROCESSOR_TEST_EXTRACT_INFO_X', 123456 );
-               }
-
-               $processor = new ExtensionProcessor();
-               $processor->extractInfo( $this->dir, self::$default + [
-                       'namespaces' => [
-                               [
-                                       'id' => 332200,
-                                       'constant' => 'MW_EXTENSION_PROCESSOR_TEST_EXTRACT_INFO_A',
-                                       'name' => 'Test_A',
-                                       'defaultcontentmodel' => 'TestModel',
-                                       'gender' => [
-                                               'male' => 'Male test',
-                                               'female' => 'Female test',
-                                       ],
-                                       'subpages' => true,
-                                       'content' => true,
-                                       'protection' => 'userright',
-                               ],
-                               [ // Test_X will use ID 123456 not 334400
-                                       'id' => 334400,
-                                       'constant' => 'MW_EXTENSION_PROCESSOR_TEST_EXTRACT_INFO_X',
-                                       'name' => 'Test_X',
-                                       'defaultcontentmodel' => 'TestModel'
-                               ],
-                       ]
-               ], 1 );
-
-               $extracted = $processor->getExtractedInfo();
-
-               $this->assertArrayHasKey(
-                       'MW_EXTENSION_PROCESSOR_TEST_EXTRACT_INFO_A',
-                       $extracted['defines']
-               );
-               $this->assertArrayNotHasKey(
-                       'MW_EXTENSION_PROCESSOR_TEST_EXTRACT_INFO_X',
-                       $extracted['defines']
-               );
-
-               $this->assertSame(
-                       $extracted['defines']['MW_EXTENSION_PROCESSOR_TEST_EXTRACT_INFO_A'],
-                       332200
-               );
-
-               $this->assertArrayHasKey( 'ExtensionNamespaces', $extracted['attributes'] );
-               $this->assertArrayHasKey( 123456, $extracted['attributes']['ExtensionNamespaces'] );
-               $this->assertArrayHasKey( 332200, $extracted['attributes']['ExtensionNamespaces'] );
-               $this->assertArrayNotHasKey( 334400, $extracted['attributes']['ExtensionNamespaces'] );
-
-               $this->assertSame( 'Test_X', $extracted['attributes']['ExtensionNamespaces'][123456] );
-               $this->assertSame( 'Test_A', $extracted['attributes']['ExtensionNamespaces'][332200] );
-               $this->assertSame(
-                       [ 'male' => 'Male test', 'female' => 'Female test' ],
-                       $extracted['globals']['wgExtraGenderNamespaces'][332200]
-               );
-               // A has subpages, X does not
-               $this->assertTrue( $extracted['globals']['wgNamespacesWithSubpages'][332200] );
-               $this->assertArrayNotHasKey( 123456, $extracted['globals']['wgNamespacesWithSubpages'] );
-       }
-
-       public static function provideRegisterHooks() {
-               $merge = [ ExtensionRegistry::MERGE_STRATEGY => 'array_merge_recursive' ];
-               // Format:
-               // Current $wgHooks
-               // Content in extension.json
-               // Expected value of $wgHooks
-               return [
-                       // No hooks
-                       [
-                               [],
-                               self::$default,
-                               $merge,
-                       ],
-                       // No current hooks, adding one for "FooBaz" in string format
-                       [
-                               [],
-                               [ 'Hooks' => [ 'FooBaz' => 'FooBazCallback' ] ] + self::$default,
-                               [ 'FooBaz' => [ 'FooBazCallback' ] ] + $merge,
-                       ],
-                       // Hook for "FooBaz", adding another one
-                       [
-                               [ 'FooBaz' => [ 'PriorCallback' ] ],
-                               [ 'Hooks' => [ 'FooBaz' => 'FooBazCallback' ] ] + self::$default,
-                               [ 'FooBaz' => [ 'PriorCallback', 'FooBazCallback' ] ] + $merge,
-                       ],
-                       // No current hooks, adding one for "FooBaz" in verbose array format
-                       [
-                               [],
-                               [ 'Hooks' => [ 'FooBaz' => [ 'FooBazCallback' ] ] ] + self::$default,
-                               [ 'FooBaz' => [ 'FooBazCallback' ] ] + $merge,
-                       ],
-                       // Hook for "BarBaz", adding one for "FooBaz"
-                       [
-                               [ 'BarBaz' => [ 'BarBazCallback' ] ],
-                               [ 'Hooks' => [ 'FooBaz' => 'FooBazCallback' ] ] + self::$default,
-                               [
-                                       'BarBaz' => [ 'BarBazCallback' ],
-                                       'FooBaz' => [ 'FooBazCallback' ],
-                               ] + $merge,
-                       ],
-                       // Callbacks for FooBaz wrapped in an array
-                       [
-                               [],
-                               [ 'Hooks' => [ 'FooBaz' => [ 'Callback1' ] ] ] + self::$default,
-                               [
-                                       'FooBaz' => [ 'Callback1' ],
-                               ] + $merge,
-                       ],
-                       // Multiple callbacks for FooBaz hook
-                       [
-                               [],
-                               [ 'Hooks' => [ 'FooBaz' => [ 'Callback1', 'Callback2' ] ] ] + self::$default,
-                               [
-                                       'FooBaz' => [ 'Callback1', 'Callback2' ],
-                               ] + $merge,
-                       ],
-               ];
-       }
-
-       /**
-        * @dataProvider provideRegisterHooks
-        */
-       public function testRegisterHooks( $pre, $info, $expected ) {
-               $processor = new MockExtensionProcessor( [ 'wgHooks' => $pre ] );
-               $processor->extractInfo( $this->dir, $info, 1 );
-               $extracted = $processor->getExtractedInfo();
-               $this->assertEquals( $expected, $extracted['globals']['wgHooks'] );
-       }
-
-       public function testExtractConfig1() {
-               $processor = new ExtensionProcessor;
-               $info = [
-                       'config' => [
-                               'Bar' => 'somevalue',
-                               'Foo' => 10,
-                               '@IGNORED' => 'yes',
-                       ],
-               ] + self::$default;
-               $info2 = [
-                       'config' => [
-                               '_prefix' => 'eg',
-                               'Bar' => 'somevalue'
-                       ],
-                       'name' => 'FooBar2',
-               ];
-               $processor->extractInfo( $this->dir, $info, 1 );
-               $processor->extractInfo( $this->dir, $info2, 1 );
-               $extracted = $processor->getExtractedInfo();
-               $this->assertEquals( 'somevalue', $extracted['globals']['wgBar'] );
-               $this->assertEquals( 10, $extracted['globals']['wgFoo'] );
-               $this->assertArrayNotHasKey( 'wg@IGNORED', $extracted['globals'] );
-               // Custom prefix:
-               $this->assertEquals( 'somevalue', $extracted['globals']['egBar'] );
-       }
-
-       public function testExtractConfig2() {
-               $processor = new ExtensionProcessor;
-               $info = [
-                       'config' => [
-                               'Bar' => [ 'value' => 'somevalue' ],
-                               'Foo' => [ 'value' => 10 ],
-                               'Path' => [ 'value' => 'foo.txt', 'path' => true ],
-                               'Namespaces' => [
-                                       'value' => [
-                                               '10' => true,
-                                               '12' => false,
-                                       ],
-                                       'merge_strategy' => 'array_plus',
-                               ],
-                       ],
-               ] + self::$default;
-               $info2 = [
-                       'config' => [
-                               'Bar' => [ 'value' => 'somevalue' ],
-                       ],
-                       'config_prefix' => 'eg',
-                       'name' => 'FooBar2',
-               ];
-               $processor->extractInfo( $this->dir, $info, 2 );
-               $processor->extractInfo( $this->dir, $info2, 2 );
-               $extracted = $processor->getExtractedInfo();
-               $this->assertEquals( 'somevalue', $extracted['globals']['wgBar'] );
-               $this->assertEquals( 10, $extracted['globals']['wgFoo'] );
-               $this->assertEquals( "{$this->dirname}/foo.txt", $extracted['globals']['wgPath'] );
-               // Custom prefix:
-               $this->assertEquals( 'somevalue', $extracted['globals']['egBar'] );
-               $this->assertSame(
-                       [ 10 => true, 12 => false, ExtensionRegistry::MERGE_STRATEGY => 'array_plus' ],
-                       $extracted['globals']['wgNamespaces']
-               );
-       }
-
-       /**
-        * @expectedException RuntimeException
-        */
-       public function testDuplicateConfigKey1() {
-               $processor = new ExtensionProcessor;
-               $info = [
-                       'config' => [
-                               'Bar' => '',
-                       ]
-               ] + self::$default;
-               $info2 = [
-                       'config' => [
-                               'Bar' => 'g',
-                       ],
-                       'name' => 'FooBar2',
-               ];
-               $processor->extractInfo( $this->dir, $info, 1 );
-               $processor->extractInfo( $this->dir, $info2, 1 );
-       }
-
-       /**
-        * @expectedException RuntimeException
-        */
-       public function testDuplicateConfigKey2() {
-               $processor = new ExtensionProcessor;
-               $info = [
-                       'config' => [
-                               'Bar' => [ 'value' => 'somevalue' ],
-                       ]
-               ] + self::$default;
-               $info2 = [
-                       'config' => [
-                               'Bar' => [ 'value' => 'somevalue' ],
-                       ],
-                       'name' => 'FooBar2',
-               ];
-               $processor->extractInfo( $this->dir, $info, 2 );
-               $processor->extractInfo( $this->dir, $info2, 2 );
-       }
-
-       public static function provideExtractExtensionMessagesFiles() {
-               $dir = __DIR__ . '/FooBar/';
-               return [
-                       [
-                               [ 'ExtensionMessagesFiles' => [ 'FooBarAlias' => 'FooBar.alias.php' ] ],
-                               [ 'wgExtensionMessagesFiles' => [ 'FooBarAlias' => $dir . 'FooBar.alias.php' ] ]
-                       ],
-                       [
-                               [
-                                       'ExtensionMessagesFiles' => [
-                                               'FooBarAlias' => 'FooBar.alias.php',
-                                               'FooBarMagic' => 'FooBar.magic.i18n.php',
-                                       ],
-                               ],
-                               [
-                                       'wgExtensionMessagesFiles' => [
-                                               'FooBarAlias' => $dir . 'FooBar.alias.php',
-                                               'FooBarMagic' => $dir . 'FooBar.magic.i18n.php',
-                                       ],
-                               ],
-                       ],
-               ];
-       }
-
-       /**
-        * @dataProvider provideExtractExtensionMessagesFiles
-        */
-       public function testExtractExtensionMessagesFiles( $input, $expected ) {
-               $processor = new ExtensionProcessor();
-               $processor->extractInfo( $this->dir, $input + self::$default, 1 );
-               $out = $processor->getExtractedInfo();
-               foreach ( $expected as $key => $value ) {
-                       $this->assertEquals( $value, $out['globals'][$key] );
-               }
-       }
-
-       public static function provideExtractMessagesDirs() {
-               $dir = __DIR__ . '/FooBar/';
-               return [
-                       [
-                               [ 'MessagesDirs' => [ 'VisualEditor' => 'i18n' ] ],
-                               [ 'wgMessagesDirs' => [ 'VisualEditor' => [ $dir . 'i18n' ] ] ]
-                       ],
-                       [
-                               [ 'MessagesDirs' => [ 'VisualEditor' => [ 'i18n', 'foobar' ] ] ],
-                               [ 'wgMessagesDirs' => [ 'VisualEditor' => [ $dir . 'i18n', $dir . 'foobar' ] ] ]
-                       ],
-               ];
-       }
-
-       /**
-        * @dataProvider provideExtractMessagesDirs
-        */
-       public function testExtractMessagesDirs( $input, $expected ) {
-               $processor = new ExtensionProcessor();
-               $processor->extractInfo( $this->dir, $input + self::$default, 1 );
-               $out = $processor->getExtractedInfo();
-               foreach ( $expected as $key => $value ) {
-                       $this->assertEquals( $value, $out['globals'][$key] );
-               }
-       }
-
-       public function testExtractCredits() {
-               $processor = new ExtensionProcessor();
-               $processor->extractInfo( $this->dir, self::$default, 1 );
-               $this->setExpectedException( Exception::class );
-               $processor->extractInfo( $this->dir, self::$default, 1 );
-       }
-
-       /**
-        * @dataProvider provideExtractResourceLoaderModules
-        */
-       public function testExtractResourceLoaderModules(
-               $input,
-               array $expectedGlobals,
-               array $expectedAttribs = []
-       ) {
-               $processor = new ExtensionProcessor();
-               $processor->extractInfo( $this->dir, $input + self::$default, 1 );
-               $out = $processor->getExtractedInfo();
-               foreach ( $expectedGlobals as $key => $value ) {
-                       $this->assertEquals( $value, $out['globals'][$key] );
-               }
-               foreach ( $expectedAttribs as $key => $value ) {
-                       $this->assertEquals( $value, $out['attributes'][$key] );
-               }
-       }
-
-       public static function provideExtractResourceLoaderModules() {
-               $dir = __DIR__ . '/FooBar';
-               return [
-                       // Generic module with localBasePath/remoteExtPath specified
-                       [
-                               // Input
-                               [
-                                       'ResourceModules' => [
-                                               'test.foo' => [
-                                                       'styles' => 'foobar.js',
-                                                       'localBasePath' => '',
-                                                       'remoteExtPath' => 'FooBar',
-                                               ],
-                                       ],
-                               ],
-                               // Expected
-                               [
-                                       'wgResourceModules' => [
-                                               'test.foo' => [
-                                                       'styles' => 'foobar.js',
-                                                       'localBasePath' => $dir,
-                                                       'remoteExtPath' => 'FooBar',
-                                               ],
-                                       ],
-                               ],
-                       ],
-                       // ResourceFileModulePaths specified:
-                       [
-                               // Input
-                               [
-                                       'ResourceFileModulePaths' => [
-                                               'localBasePath' => 'modules',
-                                               'remoteExtPath' => 'FooBar/modules',
-                                       ],
-                                       'ResourceModules' => [
-                                               // No paths
-                                               'test.foo' => [
-                                                       'styles' => 'foo.js',
-                                               ],
-                                               // Different paths set
-                                               'test.bar' => [
-                                                       'styles' => 'bar.js',
-                                                       'localBasePath' => 'subdir',
-                                                       'remoteExtPath' => 'FooBar/subdir',
-                                               ],
-                                               // Custom class with no paths set
-                                               'test.class' => [
-                                                       'class' => 'FooBarModule',
-                                                       'extra' => 'argument',
-                                               ],
-                                               // Custom class with a localBasePath
-                                               'test.class.with.path' => [
-                                                       'class' => 'FooBarPathModule',
-                                                       'extra' => 'argument',
-                                                       'localBasePath' => '',
-                                               ]
-                                       ],
-                               ],
-                               // Expected
-                               [
-                                       'wgResourceModules' => [
-                                               'test.foo' => [
-                                                       'styles' => 'foo.js',
-                                                       'localBasePath' => "$dir/modules",
-                                                       'remoteExtPath' => 'FooBar/modules',
-                                               ],
-                                               'test.bar' => [
-                                                       'styles' => 'bar.js',
-                                                       'localBasePath' => "$dir/subdir",
-                                                       'remoteExtPath' => 'FooBar/subdir',
-                                               ],
-                                               'test.class' => [
-                                                       'class' => 'FooBarModule',
-                                                       'extra' => 'argument',
-                                                       'localBasePath' => "$dir/modules",
-                                                       'remoteExtPath' => 'FooBar/modules',
-                                               ],
-                                               'test.class.with.path' => [
-                                                       'class' => 'FooBarPathModule',
-                                                       'extra' => 'argument',
-                                                       'localBasePath' => $dir,
-                                                       'remoteExtPath' => 'FooBar/modules',
-                                               ]
-                                       ],
-                               ],
-                       ],
-                       // ResourceModuleSkinStyles with file module paths
-                       [
-                               // Input
-                               [
-                                       'ResourceFileModulePaths' => [
-                                               'localBasePath' => '',
-                                               'remoteSkinPath' => 'FooBar',
-                                       ],
-                                       'ResourceModuleSkinStyles' => [
-                                               'foobar' => [
-                                                       'test.foo' => 'foo.css',
-                                               ]
-                                       ],
-                               ],
-                               // Expected
-                               [
-                                       'wgResourceModuleSkinStyles' => [
-                                               'foobar' => [
-                                                       'test.foo' => 'foo.css',
-                                                       'localBasePath' => $dir,
-                                                       'remoteSkinPath' => 'FooBar',
-                                               ],
-                                       ],
-                               ],
-                       ],
-                       // ResourceModuleSkinStyles with file module paths and an override
-                       [
-                               // Input
-                               [
-                                       'ResourceFileModulePaths' => [
-                                               'localBasePath' => '',
-                                               'remoteSkinPath' => 'FooBar',
-                                       ],
-                                       'ResourceModuleSkinStyles' => [
-                                               'foobar' => [
-                                                       'test.foo' => 'foo.css',
-                                                       'remoteSkinPath' => 'BarFoo'
-                                               ],
-                                       ],
-                               ],
-                               // Expected
-                               [
-                                       'wgResourceModuleSkinStyles' => [
-                                               'foobar' => [
-                                                       'test.foo' => 'foo.css',
-                                                       'localBasePath' => $dir,
-                                                       'remoteSkinPath' => 'BarFoo',
-                                               ],
-                                       ],
-                               ],
-                       ],
-                       'QUnit test module' => [
-                               // Input
-                               [
-                                       'QUnitTestModule' => [
-                                               'localBasePath' => '',
-                                               'remoteExtPath' => 'Foo',
-                                               'scripts' => 'bar.js',
-                                       ],
-                               ],
-                               // Expected
-                               [],
-                               [
-                                       'QUnitTestModules' => [
-                                               'test.FooBar' => [
-                                                       'localBasePath' => $dir,
-                                                       'remoteExtPath' => 'Foo',
-                                                       'scripts' => 'bar.js',
-                                               ],
-                                       ],
-                               ],
-                       ],
-               ];
-       }
-
-       public static function provideSetToGlobal() {
-               return [
-                       [
-                               [ 'wgAPIModules', 'wgAvailableRights' ],
-                               [],
-                               [
-                                       'APIModules' => [ 'foobar' => 'ApiFooBar' ],
-                                       'AvailableRights' => [ 'foobar', 'unfoobar' ],
-                               ],
-                               [
-                                       'wgAPIModules' => [ 'foobar' => 'ApiFooBar' ],
-                                       'wgAvailableRights' => [ 'foobar', 'unfoobar' ],
-                               ],
-                       ],
-                       [
-                               [ 'wgAPIModules', 'wgAvailableRights' ],
-                               [
-                                       'wgAPIModules' => [ 'barbaz' => 'ApiBarBaz' ],
-                                       'wgAvailableRights' => [ 'barbaz' ]
-                               ],
-                               [
-                                       'APIModules' => [ 'foobar' => 'ApiFooBar' ],
-                                       'AvailableRights' => [ 'foobar', 'unfoobar' ],
-                               ],
-                               [
-                                       'wgAPIModules' => [ 'barbaz' => 'ApiBarBaz', 'foobar' => 'ApiFooBar' ],
-                                       'wgAvailableRights' => [ 'barbaz', 'foobar', 'unfoobar' ],
-                               ],
-                       ],
-                       [
-                               [ 'wgGroupPermissions' ],
-                               [
-                                       'wgGroupPermissions' => [
-                                               'sysop' => [ 'delete' ]
-                                       ],
-                               ],
-                               [
-                                       'GroupPermissions' => [
-                                               'sysop' => [ 'undelete' ],
-                                               'user' => [ 'edit' ]
-                                       ],
-                               ],
-                               [
-                                       'wgGroupPermissions' => [
-                                               'sysop' => [ 'delete', 'undelete' ],
-                                               'user' => [ 'edit' ]
-                                       ],
-                               ]
-                       ]
-               ];
-       }
-
-       /**
-        * Attributes under manifest_version 2
-        */
-       public function testExtractAttributes() {
-               $processor = new ExtensionProcessor();
-               // Load FooBar extension
-               $processor->extractInfo( $this->dir, [ 'name' => 'FooBar' ], 2 );
-               $processor->extractInfo(
-                       $this->dir,
-                       [
-                               'name' => 'Baz',
-                               'attributes' => [
-                                       // Loaded
-                                       'FooBar' => [
-                                               'Plugins' => [
-                                                       'ext.baz.foobar',
-                                               ],
-                                       ],
-                                       // Not loaded
-                                       'FizzBuzz' => [
-                                               'MorePlugins' => [
-                                                       'ext.baz.fizzbuzz',
-                                               ],
-                                       ],
-                               ],
-                       ],
-                       2
-               );
-
-               $info = $processor->getExtractedInfo();
-               $this->assertArrayHasKey( 'FooBarPlugins', $info['attributes'] );
-               $this->assertSame( [ 'ext.baz.foobar' ], $info['attributes']['FooBarPlugins'] );
-               $this->assertArrayNotHasKey( 'FizzBuzzMorePlugins', $info['attributes'] );
-       }
-
-       /**
-        * Attributes under manifest_version 1
-        */
-       public function testAttributes1() {
-               $processor = new ExtensionProcessor();
-               $processor->extractInfo(
-                       $this->dir,
-                       [
-                               'name' => 'FooBar',
-                               'FooBarPlugins' => [
-                                       'ext.baz.foobar',
-                               ],
-                               'FizzBuzzMorePlugins' => [
-                                       'ext.baz.fizzbuzz',
-                               ],
-                       ],
-                       1
-               );
-               $processor->extractInfo(
-                       $this->dir,
-                       [
-                               'name' => 'FooBar2',
-                               'FizzBuzzMorePlugins' => [
-                                       'ext.bar.fizzbuzz',
-                               ]
-                       ],
-                       1
-               );
-
-               $info = $processor->getExtractedInfo();
-               $this->assertArrayHasKey( 'FooBarPlugins', $info['attributes'] );
-               $this->assertSame( [ 'ext.baz.foobar' ], $info['attributes']['FooBarPlugins'] );
-               $this->assertArrayHasKey( 'FizzBuzzMorePlugins', $info['attributes'] );
-               $this->assertSame(
-                       [ 'ext.baz.fizzbuzz', 'ext.bar.fizzbuzz' ],
-                       $info['attributes']['FizzBuzzMorePlugins']
-               );
-       }
-
-       public function testAttributes1_notarray() {
-               $processor = new ExtensionProcessor();
-               $this->setExpectedException(
-                       InvalidArgumentException::class,
-                       "The value for 'FooBarPlugins' should be an array (from {$this->dir})"
-               );
-               $processor->extractInfo(
-                       $this->dir,
-                       [
-                               'FooBarPlugins' => 'ext.baz.foobar',
-                       ] + self::$default,
-                       1
-               );
-       }
-
-       public function testExtractPathBasedGlobal() {
-               $processor = new ExtensionProcessor();
-               $processor->extractInfo(
-                       $this->dir,
-                       [
-                               'ParserTestFiles' => [
-                                       'tests/parserTests.txt',
-                                       'tests/extraParserTests.txt',
-                               ],
-                               'ServiceWiringFiles' => [
-                                       'includes/ServiceWiring.php'
-                               ],
-                       ] + self::$default,
-                       1
-               );
-               $globals = $processor->getExtractedInfo()['globals'];
-               $this->assertArrayHasKey( 'wgParserTestFiles', $globals );
-               $this->assertSame( [
-                       "{$this->dirname}/tests/parserTests.txt",
-                       "{$this->dirname}/tests/extraParserTests.txt"
-               ], $globals['wgParserTestFiles'] );
-               $this->assertArrayHasKey( 'wgServiceWiringFiles', $globals );
-               $this->assertSame( [
-                       "{$this->dirname}/includes/ServiceWiring.php"
-               ], $globals['wgServiceWiringFiles'] );
-       }
-
-       public function testGetRequirements() {
-               $info = self::$default + [
-                       'requires' => [
-                               'MediaWiki' => '>= 1.25.0',
-                               'platform' => [
-                                       'php' => '>= 5.5.9'
-                               ],
-                               'extensions' => [
-                                       'Bar' => '*'
-                               ]
-                       ]
-               ];
-               $processor = new ExtensionProcessor();
-               $this->assertSame(
-                       $info['requires'],
-                       $processor->getRequirements( $info, false )
-               );
-               $this->assertSame(
-                       [],
-                       $processor->getRequirements( [], false )
-               );
-       }
-
-       public function testGetDevRequirements() {
-               $info = self::$default + [
-                       'dev-requires' => [
-                               'MediaWiki' => '>= 1.31.0',
-                               'platform' => [
-                                       'ext-foo' => '*',
-                               ],
-                               'skins' => [
-                                       'Baz' => '*',
-                               ],
-                               'extensions' => [
-                                       'Biz' => '*',
-                               ],
-                       ],
-               ];
-               $processor = new ExtensionProcessor();
-               $this->assertSame(
-                       $info['dev-requires'],
-                       $processor->getRequirements( $info, true )
-               );
-               // Set some standard requirements, so we can test merging
-               $info['requires'] = [
-                       'MediaWiki' => '>= 1.25.0',
-                       'platform' => [
-                               'php' => '>= 5.5.9'
-                       ],
-                       'extensions' => [
-                               'Bar' => '*'
-                       ]
-               ];
-               $this->assertSame(
-                       [
-                               'MediaWiki' => '>= 1.25.0 >= 1.31.0',
-                               'platform' => [
-                                       'php' => '>= 5.5.9',
-                                       'ext-foo' => '*',
-                               ],
-                               'extensions' => [
-                                       'Bar' => '*',
-                                       'Biz' => '*',
-                               ],
-                               'skins' => [
-                                       'Baz' => '*',
-                               ],
-                       ],
-                       $processor->getRequirements( $info, true )
-               );
-
-               // If there's no dev-requires, it just returns requires
-               unset( $info['dev-requires'] );
-               $this->assertSame(
-                       $info['requires'],
-                       $processor->getRequirements( $info, true )
-               );
-       }
-
-       public function testGetExtraAutoloaderPaths() {
-               $processor = new ExtensionProcessor();
-               $this->assertSame(
-                       [ "{$this->dirname}/vendor/autoload.php" ],
-                       $processor->getExtraAutoloaderPaths( $this->dirname, [
-                               'load_composer_autoloader' => true,
-                       ] )
-               );
-       }
-
-       /**
-        * Verify that extension.schema.json is in sync with ExtensionProcessor
-        *
-        * @coversNothing
-        */
-       public function testGlobalSettingsDocumentedInSchema() {
-               global $IP;
-               $globalSettings = TestingAccessWrapper::newFromClass(
-                       ExtensionProcessor::class )->globalSettings;
-
-               $version = ExtensionRegistry::MANIFEST_VERSION;
-               $schema = FormatJson::decode(
-                       file_get_contents( "$IP/docs/extension.schema.v$version.json" ),
-                       true
-               );
-               $missing = [];
-               foreach ( $globalSettings as $global ) {
-                       if ( !isset( $schema['properties'][$global] ) ) {
-                               $missing[] = $global;
-                       }
-               }
-
-               $this->assertEquals( [], $missing,
-                       "The following global settings are not documented in docs/extension.schema.json" );
-       }
-}
-
-/**
- * Allow overriding the default value of $this->globals
- * so we can test merging
- */
-class MockExtensionProcessor extends ExtensionProcessor {
-       public function __construct( $globals = [] ) {
-               $this->globals = $globals + $this->globals;
-       }
-}
diff --git a/tests/phpunit/includes/registration/VersionCheckerTest.php b/tests/phpunit/includes/registration/VersionCheckerTest.php
deleted file mode 100644 (file)
index e824e3f..0000000
+++ /dev/null
@@ -1,479 +0,0 @@
-<?php
-
-/**
- * @covers VersionChecker
- */
-class VersionCheckerTest extends PHPUnit\Framework\TestCase {
-
-       use MediaWikiCoversValidator;
-       use PHPUnit4And6Compat;
-
-       /**
-        * @dataProvider provideMediaWikiCheck
-        */
-       public function testMediaWikiCheck( $coreVersion, $constraint, $expected ) {
-               $checker = new VersionChecker( $coreVersion, '7.0.0', [] );
-               $this->assertEquals( $expected, !(bool)$checker->checkArray( [
-                       'FakeExtension' => [
-                               'MediaWiki' => $constraint,
-                       ],
-               ] ) );
-       }
-
-       public static function provideMediaWikiCheck() {
-               return [
-                       // [ $wgVersion, constraint, expected ]
-                       [ '1.25alpha', '>= 1.26', false ],
-                       [ '1.25.0', '>= 1.26', false ],
-                       [ '1.26alpha', '>= 1.26', true ],
-                       [ '1.26alpha', '>= 1.26.0', true ],
-                       [ '1.26alpha', '>= 1.26.0-stable', false ],
-                       [ '1.26.0', '>= 1.26.0-stable', true ],
-                       [ '1.26.1', '>= 1.26.0-stable', true ],
-                       [ '1.27.1', '>= 1.26.0-stable', true ],
-                       [ '1.26alpha', '>= 1.26.1', false ],
-                       [ '1.26alpha', '>= 1.26alpha', true ],
-                       [ '1.26alpha', '>= 1.25', true ],
-                       [ '1.26.0-alpha.14', '>= 1.26.0-alpha.15', false ],
-                       [ '1.26.0-alpha.14', '>= 1.26.0-alpha.10', true ],
-                       [ '1.26.1', '>= 1.26.2, <=1.26.0', false ],
-                       [ '1.26.1', '^1.26.2', false ],
-                       // Accept anything for un-parsable version strings
-                       [ '1.26mwf14', '== 1.25alpha', true ],
-                       [ 'totallyinvalid', '== 1.0', true ],
-               ];
-       }
-
-       /**
-        * @dataProvider providePhpValidCheck
-        */
-       public function testPhpValidCheck( $phpVersion, $constraint, $expected ) {
-               $checker = new VersionChecker( '1.0.0', $phpVersion, [] );
-               $this->assertEquals( $expected, !(bool)$checker->checkArray( [
-                       'FakeExtension' => [
-                               'platform' => [
-                                       'php' => $constraint,
-                               ],
-                       ],
-               ] ) );
-       }
-
-       public static function providePhpValidCheck() {
-               return [
-                       // [ phpVersion, constraint, expected ]
-                       [ '7.0.23', '>= 7.0.0', true ],
-                       [ '7.0.23', '^7.1.0', false ],
-                       [ '7.0.23', '7.0.23', true ],
-               ];
-       }
-
-       /**
-        * @expectedException UnexpectedValueException
-        */
-       public function testPhpInvalidConstraint() {
-               $checker = new VersionChecker( '1.0.0', '7.0.0', [] );
-               $checker->checkArray( [
-                       'FakeExtension' => [
-                               'platform' => [
-                                       'php' => 'totallyinvalid',
-                               ],
-                       ],
-               ] );
-       }
-
-       /**
-        * @dataProvider providePhpInvalidVersion
-        * @expectedException UnexpectedValueException
-        */
-       public function testPhpInvalidVersion( $phpVersion ) {
-                $checker = new VersionChecker( '1.0.0', $phpVersion, [] );
-       }
-
-       public static function providePhpInvalidVersion() {
-               return [
-                       // [ phpVersion ]
-                       [ '7.abc' ],
-                       [ '5.a.x' ],
-               ];
-       }
-
-       /**
-        * @dataProvider provideType
-        */
-       public function testType( $given, $expected ) {
-               $checker = new VersionChecker(
-                       '1.0.0',
-                       '7.0.0',
-                       [ 'phpLoadedExtension' ],
-                       [
-                               'presentAbility' => true,
-                               'presentAbilityWithMessage' => true,
-                               'missingAbility' => false,
-                               'missingAbilityWithMessage' => false,
-                       ],
-                       [
-                               'presentAbilityWithMessage' => 'Present.',
-                               'missingAbilityWithMessage' => 'Missing.',
-                       ]
-               );
-               $checker->setLoadedExtensionsAndSkins( [
-                               'FakeDependency' => [
-                                       'version' => '1.0.0',
-                               ],
-                               'NoVersionGiven' => [],
-                       ] );
-               $this->assertEquals( $expected, $checker->checkArray( [
-                       'FakeExtension' => $given,
-               ] ) );
-       }
-
-       public static function provideType() {
-               return [
-                       // valid type
-                       [
-                               [
-                                       'extensions' => [
-                                               'FakeDependency' => '1.0.0',
-                                       ],
-                               ],
-                               [],
-                       ],
-                       [
-                               [
-                                       'MediaWiki' => '1.0.0',
-                               ],
-                               [],
-                       ],
-                       [
-                               [
-                                       'extensions' => [
-                                               'NoVersionGiven' => '*',
-                                       ],
-                               ],
-                               [],
-                       ],
-                       [
-                               [
-                                       'extensions' => [
-                                               'NoVersionGiven' => '1.0',
-                                       ],
-                               ],
-                               [
-                                       [
-                                               'incompatible' => 'FakeExtension',
-                                               'type' => 'incompatible-extensions',
-                                               'msg' => 'NoVersionGiven does not expose its version, but FakeExtension requires: 1.0.',
-                                       ],
-                               ],
-                       ],
-                       [
-                               [
-                                       'extensions' => [
-                                               'Missing' => '*',
-                                       ],
-                               ],
-                               [
-                                       [
-                                               'missing' => 'Missing',
-                                               'type' => 'missing-extensions',
-                                               'msg' => 'FakeExtension requires Missing to be installed.',
-                                       ],
-                               ],
-                       ],
-                       [
-                               [
-                                       'extensions' => [
-                                               'FakeDependency' => '2.0.0',
-                                       ],
-                               ],
-                               [
-                                       [
-                                               'incompatible' => 'FakeExtension',
-                                               'type' => 'incompatible-extensions',
-                                               // phpcs:ignore Generic.Files.LineLength.TooLong
-                                               'msg' => 'FakeExtension is not compatible with the current installed version of FakeDependency (1.0.0), it requires: 2.0.0.',
-                                       ],
-                               ],
-                       ],
-                       [
-                               [
-                                       'skins' => [
-                                               'FakeSkin' => '*',
-                                       ],
-                               ],
-                               [
-                                       [
-                                               'missing' => 'FakeSkin',
-                                               'type' => 'missing-skins',
-                                               'msg' => 'FakeExtension requires FakeSkin to be installed.',
-                                       ],
-                               ],
-                       ],
-                       [
-                               [
-                                       'platform' => [
-                                               'ext-phpLoadedExtension' => '*',
-                                       ],
-                               ],
-                               [],
-                       ],
-                       [
-                               [
-                                       'platform' => [
-                                               'ext-phpMissingExtension' => '*',
-                                       ],
-                               ],
-                               [
-                                       [
-                                               'missing' => 'phpMissingExtension',
-                                               'type' => 'missing-phpExtension',
-                                               // phpcs:ignore Generic.Files.LineLength.TooLong
-                                               'msg' => 'FakeExtension requires phpMissingExtension PHP extension to be installed.',
-                                       ],
-                               ],
-                       ],
-                       [
-                               [
-                                       'platform' => [
-                                               'ability-presentAbility' => true,
-                                       ],
-                               ],
-                               [],
-                       ],
-                       [
-                               [
-                                       'platform' => [
-                                               'ability-presentAbilityWithMessage' => true,
-                                       ],
-                               ],
-                               [],
-                       ],
-                       [
-                               [
-                                       'platform' => [
-                                               'ability-presentAbility' => false,
-                                       ],
-                               ],
-                               [],
-                       ],
-                       [
-                               [
-                                       'platform' => [
-                                               'ability-presentAbilityWithMessage' => false,
-                                       ],
-                               ],
-                               [],
-                       ],
-                       [
-                               [
-                                       'platform' => [
-                                               'ability-missingAbility' => true,
-                                       ],
-                               ],
-                               [
-                                       [
-                                               'missing' => 'missingAbility',
-                                               'type' => 'missing-ability',
-                                               'msg' => 'FakeExtension requires "missingAbility" ability',
-                                       ],
-                               ],
-                       ],
-                       [
-                               [
-                                       'platform' => [
-                                               'ability-missingAbilityWithMessage' => true,
-                                       ],
-                               ],
-                               [
-                                       [
-                                               'missing' => 'missingAbilityWithMessage',
-                                               'type' => 'missing-ability',
-                                               // phpcs:ignore Generic.Files.LineLength.TooLong
-                                               'msg' => 'FakeExtension requires "missingAbilityWithMessage" ability: Missing.',
-                                       ],
-                               ],
-                       ],
-                       [
-                               [
-                                       'platform' => [
-                                               'ability-missingAbility' => false,
-                                       ],
-                               ],
-                               [],
-                       ],
-                       [
-                               [
-                                       'platform' => [
-                                               'ability-missingAbilityWithMessage' => false,
-                                       ],
-                               ],
-                               [],
-                       ],
-               ];
-       }
-
-       /**
-        * Check, if a non-parsable version constraint does not throw an exception or
-        * returns any error message.
-        */
-       public function testInvalidConstraint() {
-               $checker = new VersionChecker( '1.0.0', '7.0.0', [] );
-               $checker->setLoadedExtensionsAndSkins( [
-                               'FakeDependency' => [
-                                       'version' => 'not really valid',
-                               ],
-                       ] );
-               $this->assertEquals( [
-                       [
-                               'type' => 'invalid-version',
-                               'msg' => "FakeDependency does not have a valid version string.",
-                       ],
-               ], $checker->checkArray( [
-                       'FakeExtension' => [
-                               'extensions' => [
-                                       'FakeDependency' => '1.24.3',
-                               ],
-                       ],
-               ] ) );
-
-               $checker = new VersionChecker( '1.0.0', '7.0.0', [] );
-               $checker->setLoadedExtensionsAndSkins( [
-                               'FakeDependency' => [
-                                       'version' => '1.24.3',
-                               ],
-                       ] );
-
-               $this->setExpectedException( UnexpectedValueException::class );
-               $checker->checkArray( [
-                       'FakeExtension' => [
-                               'FakeDependency' => 'not really valid',
-                       ],
-               ] );
-       }
-
-       public function provideInvalidDependency() {
-               return [
-                       [
-                               [
-                                       'FakeExtension' => [
-                                               'platform' => [
-                                                       'undefinedPlatformDependency' => '*',
-                                               ],
-                                       ],
-                               ],
-                               'undefinedPlatformDependency',
-                       ],
-                       [
-                               [
-                                       'FakeExtension' => [
-                                               'platform' => [
-                                                       'phpLoadedExtension' => '*',
-                                               ],
-                                       ],
-                               ],
-                               'phpLoadedExtension',
-                       ],
-                       [
-                               [
-                                       'FakeExtension' => [
-                                               'platform' => [
-                                                       'ability-invalidAbility' => true,
-                                               ],
-                                       ],
-                               ],
-                               'ability-invalidAbility',
-                       ],
-                       [
-                               [
-                                       'FakeExtension' => [
-                                               'platform' => [
-                                                       'presentAbility' => true,
-                                               ],
-                                       ],
-                               ],
-                               'presentAbility',
-                       ],
-                       [
-                               [
-                                       'FakeExtension' => [
-                                               'undefinedDependencyType' => '*',
-                                       ],
-                               ],
-                               'undefinedDependencyType',
-                       ],
-                       // T197478
-                       [
-                               [
-                                       'FakeExtension' => [
-                                               'skin' => [
-                                                       'FakeSkin' => '*',
-                                               ],
-                                       ],
-                               ],
-                               'skin',
-                       ],
-               ];
-       }
-
-       /**
-        * @dataProvider provideInvalidDependency
-        */
-       public function testInvalidDependency( $depencency, $type ) {
-               $checker = new VersionChecker(
-                       '1.0.0',
-                       '7.0.0',
-                       [ 'phpLoadedExtension' ],
-                       [
-                               'presentAbility' => true,
-                               'missingAbility' => false,
-                       ]
-               );
-               $this->setExpectedException(
-                       UnexpectedValueException::class,
-                       "Dependency type $type unknown in FakeExtension"
-               );
-               $checker->checkArray( $depencency );
-       }
-
-       public function testInvalidPhpExtensionConstraint() {
-               $checker = new VersionChecker( '1.0.0', '7.0.0', [ 'phpLoadedExtension' ] );
-               $this->setExpectedException(
-                       UnexpectedValueException::class,
-                       'Version constraints for PHP extensions are not supported in FakeExtension'
-               );
-               $checker->checkArray( [
-                       'FakeExtension' => [
-                               'platform' => [
-                                       'ext-phpLoadedExtension' => '1.0.0',
-                               ],
-                       ],
-               ] );
-       }
-
-       /**
-        * @dataProvider provideInvalidAbilityType
-        */
-       public function testInvalidAbilityType( $value ) {
-               $checker = new VersionChecker( '1.0.0', '7.0.0', [], [ 'presentAbility' => true ] );
-               $this->setExpectedException(
-                       UnexpectedValueException::class,
-                       'Only booleans are allowed to to indicate the presence of abilities in FakeExtension'
-               );
-               $checker->checkArray( [
-                       'FakeExtension' => [
-                               'platform' => [
-                                       'ability-presentAbility' => $value,
-                               ],
-                       ],
-               ] );
-       }
-
-       public function provideInvalidAbilityType() {
-               return [
-                       [ null ],
-                       [ 1 ],
-                       [ '1' ],
-               ];
-       }
-
-}
diff --git a/tests/phpunit/includes/resourceloader/DerivativeResourceLoaderContextTest.php b/tests/phpunit/includes/resourceloader/DerivativeResourceLoaderContextTest.php
deleted file mode 100644 (file)
index e178e96..0000000
+++ /dev/null
@@ -1,138 +0,0 @@
-<?php
-
-/**
- * @group ResourceLoader
- * @covers DerivativeResourceLoaderContext
- */
-class DerivativeResourceLoaderContextTest extends PHPUnit\Framework\TestCase {
-
-       use MediaWikiCoversValidator;
-
-       protected static function makeContext() {
-               $request = new FauxRequest( [
-                               'lang' => 'qqx',
-                               'modules' => 'test.default',
-                               'only' => 'scripts',
-                               'skin' => 'fallback',
-                               'target' => 'test',
-               ] );
-               return new ResourceLoaderContext(
-                       new ResourceLoader( ResourceLoaderTestCase::getMinimalConfig() ),
-                       $request
-               );
-       }
-
-       public function testChangeModules() {
-               $derived = new DerivativeResourceLoaderContext( self::makeContext() );
-               $this->assertSame( $derived->getModules(), [ 'test.default' ], 'inherit from parent' );
-
-               $derived->setModules( [ 'test.override' ] );
-               $this->assertSame( $derived->getModules(), [ 'test.override' ] );
-       }
-
-       public function testChangeLanguageAndDirection() {
-               $derived = new DerivativeResourceLoaderContext( self::makeContext() );
-               $this->assertSame( $derived->getLanguage(), 'qqx', 'inherit from parent' );
-               $this->assertSame( $derived->getDirection(), 'ltr', 'inherit from parent' );
-
-               $derived->setLanguage( 'nl' );
-               $this->assertSame( $derived->getLanguage(), 'nl' );
-               $this->assertSame( $derived->getDirection(), 'ltr' );
-
-               // Changing the language must clear cache of computed direction
-               $derived->setLanguage( 'he' );
-               $this->assertSame( $derived->getDirection(), 'rtl' );
-               $this->assertSame( $derived->getLanguage(), 'he' );
-
-               // Overriding the direction explicitly is allowed
-               $derived->setDirection( 'ltr' );
-               $this->assertSame( $derived->getDirection(), 'ltr' );
-               $this->assertSame( $derived->getLanguage(), 'he' );
-       }
-
-       public function testChangeSkin() {
-               $derived = new DerivativeResourceLoaderContext( self::makeContext() );
-               $this->assertSame( $derived->getSkin(), 'fallback', 'inherit from parent' );
-
-               $derived->setSkin( 'myskin' );
-               $this->assertSame( $derived->getSkin(), 'myskin' );
-       }
-
-       public function testChangeUser() {
-               $derived = new DerivativeResourceLoaderContext( self::makeContext() );
-               $this->assertSame( $derived->getUser(), null, 'inherit from parent' );
-
-               $derived->setUser( 'MyUser' );
-               $this->assertSame( $derived->getUser(), 'MyUser' );
-       }
-
-       public function testChangeDebug() {
-               $derived = new DerivativeResourceLoaderContext( self::makeContext() );
-               $this->assertSame( $derived->getDebug(), false, 'inherit from parent' );
-
-               $derived->setDebug( true );
-               $this->assertSame( $derived->getDebug(), true );
-       }
-
-       public function testChangeOnly() {
-               $derived = new DerivativeResourceLoaderContext( self::makeContext() );
-               $this->assertSame( $derived->getOnly(), 'scripts', 'inherit from parent' );
-
-               $derived->setOnly( 'styles' );
-               $this->assertSame( $derived->getOnly(), 'styles' );
-
-               $derived->setOnly( null );
-               $this->assertSame( $derived->getOnly(), null );
-       }
-
-       public function testChangeVersion() {
-               $derived = new DerivativeResourceLoaderContext( self::makeContext() );
-               $this->assertSame( $derived->getVersion(), null );
-
-               $derived->setVersion( 'hw1' );
-               $this->assertSame( $derived->getVersion(), 'hw1' );
-       }
-
-       public function testChangeRaw() {
-               $derived = new DerivativeResourceLoaderContext( self::makeContext() );
-               $this->assertSame( $derived->getRaw(), false, 'inherit from parent' );
-
-               $derived->setRaw( true );
-               $this->assertSame( $derived->getRaw(), true );
-       }
-
-       public function testChangeHash() {
-               $derived = new DerivativeResourceLoaderContext( self::makeContext() );
-               $this->assertSame( $derived->getHash(), 'qqx|fallback|||scripts|||||', 'inherit' );
-
-               $derived->setLanguage( 'nl' );
-               $derived->setUser( 'Example' );
-               // Assert that subclass is able to clear parent class "hash" member
-               $this->assertSame( $derived->getHash(), 'nl|fallback||Example|scripts|||||' );
-       }
-
-       public function testChangeContentOverrides() {
-               $derived = new DerivativeResourceLoaderContext( self::makeContext() );
-               $this->assertNull( $derived->getContentOverrideCallback(), 'default' );
-
-               $override = function ( Title $t ) {
-                       return null;
-               };
-               $derived->setContentOverrideCallback( $override );
-               $this->assertSame( $override, $derived->getContentOverrideCallback(), 'changed' );
-
-               $derived2 = new DerivativeResourceLoaderContext( $derived );
-               $this->assertSame(
-                       $override,
-                       $derived2->getContentOverrideCallback(),
-                       'change via a second derivative layer'
-               );
-       }
-
-       public function testImmutableAccessors() {
-               $context = self::makeContext();
-               $derived = new DerivativeResourceLoaderContext( $context );
-               $this->assertSame( $derived->getRequest(), $context->getRequest() );
-               $this->assertSame( $derived->getResourceLoader(), $context->getResourceLoader() );
-       }
-}
diff --git a/tests/phpunit/includes/resourceloader/MessageBlobStoreTest.php b/tests/phpunit/includes/resourceloader/MessageBlobStoreTest.php
deleted file mode 100644 (file)
index e094d92..0000000
+++ /dev/null
@@ -1,197 +0,0 @@
-<?php
-
-use Wikimedia\TestingAccessWrapper;
-
-/**
- * @group ResourceLoader
- * @covers MessageBlobStore
- */
-class MessageBlobStoreTest extends PHPUnit\Framework\TestCase {
-
-       use MediaWikiCoversValidator;
-       use PHPUnit4And6Compat;
-
-       protected function setUp() {
-               parent::setUp();
-               // MediaWiki's test wrapper sets $wgMainWANCache to CACHE_NONE.
-               // Use HashBagOStuff here so that we can observe caching.
-               $this->wanCache = new WANObjectCache( [
-                       'cache' => new HashBagOStuff()
-               ] );
-
-               $this->clock = 1301655600.000;
-               $this->wanCache->setMockTime( $this->clock );
-       }
-
-       public function testBlobCreation() {
-               $module = $this->makeModule( [ 'mainpage' ] );
-               $rl = new EmptyResourceLoader();
-               $rl->register( $module->getName(), $module );
-
-               $blobStore = $this->makeBlobStore( null, $rl );
-               $blob = $blobStore->getBlob( $module, 'en' );
-
-               $this->assertEquals( '{"mainpage":"Main Page"}', $blob, 'Generated blob' );
-       }
-
-       public function testBlobCreation_empty() {
-               $module = $this->makeModule( [] );
-               $rl = new EmptyResourceLoader();
-               $rl->register( $module->getName(), $module );
-
-               $blobStore = $this->makeBlobStore( null, $rl );
-               $blob = $blobStore->getBlob( $module, 'en' );
-
-               $this->assertEquals( '{}', $blob, 'Generated blob' );
-       }
-
-       public function testBlobCreation_unknownMessage() {
-               $module = $this->makeModule( [ 'i-dont-exist', 'mainpage', 'i-dont-exist2' ] );
-               $rl = new EmptyResourceLoader();
-               $rl->register( $module->getName(), $module );
-               $blobStore = $this->makeBlobStore( null, $rl );
-
-               // Generating a blob should continue without errors,
-               // with keys of unknown messages excluded from the blob.
-               $blob = $blobStore->getBlob( $module, 'en' );
-               $this->assertEquals( '{"mainpage":"Main Page"}', $blob, 'Generated blob' );
-       }
-
-       public function testMessageCachingAndPurging() {
-               $module = $this->makeModule( [ 'example' ] );
-               $rl = new EmptyResourceLoader();
-               $rl->register( $module->getName(), $module );
-               $blobStore = $this->makeBlobStore( [ 'fetchMessage' ], $rl );
-
-               // Advance this new WANObjectCache instance to a normal state,
-               // by doing one "get" and letting its hold off period expire.
-               // Without this, the first real "get" would lazy-initialise the
-               // checkKey and thus reject the first "set".
-               $blobStore->getBlob( $module, 'en' );
-               $this->clock += 20;
-
-               // Arrange version 1 of a message
-               $blobStore->expects( $this->once() )
-                       ->method( 'fetchMessage' )
-                       ->will( $this->returnValue( 'First version' ) );
-
-               // Assert
-               $blob = $blobStore->getBlob( $module, 'en' );
-               $this->assertEquals( '{"example":"First version"}', $blob, 'Blob for v1' );
-
-               // Arrange version 2
-               $blobStore = $this->makeBlobStore( [ 'fetchMessage' ], $rl );
-               $blobStore->expects( $this->once() )
-                       ->method( 'fetchMessage' )
-                       ->will( $this->returnValue( 'Second version' ) );
-               $this->clock += 20;
-
-               // Assert
-               // We do not validate whether a cached message is up-to-date.
-               // Instead, changes to messages will send us a purge.
-               // When cache is not purged or expired, it must be used.
-               $blob = $blobStore->getBlob( $module, 'en' );
-               $this->assertEquals( '{"example":"First version"}', $blob, 'Reuse cached v1 blob' );
-
-               // Purge cache
-               $blobStore->updateMessage( 'example' );
-               $this->clock += 20;
-
-               // Assert
-               $blob = $blobStore->getBlob( $module, 'en' );
-               $this->assertEquals( '{"example":"Second version"}', $blob, 'Updated blob for v2' );
-       }
-
-       public function testPurgeEverything() {
-               $module = $this->makeModule( [ 'example' ] );
-               $rl = new EmptyResourceLoader();
-               $rl->register( $module->getName(), $module );
-               $blobStore = $this->makeBlobStore( [ 'fetchMessage' ], $rl );
-               // Advance this new WANObjectCache instance to a normal state.
-               $blobStore->getBlob( $module, 'en' );
-               $this->clock += 20;
-
-               // Arrange version 1 and 2
-               $blobStore->expects( $this->exactly( 2 ) )
-                       ->method( 'fetchMessage' )
-                       ->will( $this->onConsecutiveCalls( 'First', 'Second' ) );
-
-               // Assert
-               $blob = $blobStore->getBlob( $module, 'en' );
-               $this->assertEquals( '{"example":"First"}', $blob, 'Blob for v1' );
-
-               $this->clock += 20;
-
-               // Assert
-               $blob = $blobStore->getBlob( $module, 'en' );
-               $this->assertEquals( '{"example":"First"}', $blob, 'Blob for v1 again' );
-
-               // Purge everything
-               $blobStore->clear();
-               $this->clock += 20;
-
-               // Assert
-               $blob = $blobStore->getBlob( $module, 'en' );
-               $this->assertEquals( '{"example":"Second"}', $blob, 'Blob for v2' );
-       }
-
-       public function testValidateAgainstModuleRegistry() {
-               // Arrange version 1 of a module
-               $module = $this->makeModule( [ 'foo' ] );
-               $rl = new EmptyResourceLoader();
-               $rl->register( $module->getName(), $module );
-               $blobStore = $this->makeBlobStore( [ 'fetchMessage' ], $rl );
-               $blobStore->expects( $this->once() )
-                       ->method( 'fetchMessage' )
-                       ->will( $this->returnValueMap( [
-                               // message key, language code, message value
-                               [ 'foo', 'en', 'Hello' ],
-                       ] ) );
-
-               // Assert
-               $blob = $blobStore->getBlob( $module, 'en' );
-               $this->assertEquals( '{"foo":"Hello"}', $blob, 'Blob for v1' );
-
-               // Arrange version 2 of module
-               // While message values may be out of date, the set of messages returned
-               // must always match the set of message keys required by the module.
-               // We do not receive purges for this because no messages were changed.
-               $module = $this->makeModule( [ 'foo', 'bar' ] );
-               $rl = new EmptyResourceLoader();
-               $rl->register( $module->getName(), $module );
-               $blobStore = $this->makeBlobStore( [ 'fetchMessage' ], $rl );
-               $blobStore->expects( $this->exactly( 2 ) )
-                       ->method( 'fetchMessage' )
-                       ->will( $this->returnValueMap( [
-                               // message key, language code, message value
-                               [ 'foo', 'en', 'Hello' ],
-                               [ 'bar', 'en', 'World' ],
-                       ] ) );
-
-               // Assert
-               $blob = $blobStore->getBlob( $module, 'en' );
-               $this->assertEquals( '{"foo":"Hello","bar":"World"}', $blob, 'Blob for v2' );
-       }
-
-       public function testSetLoggedIsVoid() {
-               $blobStore = $this->makeBlobStore();
-               $this->assertSame( null, $blobStore->setLogger( new Psr\Log\NullLogger() ) );
-       }
-
-       private function makeBlobStore( $methods = null, $rl = null ) {
-               $blobStore = $this->getMockBuilder( MessageBlobStore::class )
-                       ->setConstructorArgs( [ $rl ?? $this->createMock( ResourceLoader::class ) ] )
-                       ->setMethods( $methods )
-                       ->getMock();
-
-               $access = TestingAccessWrapper::newFromObject( $blobStore );
-               $access->wanCache = $this->wanCache;
-               return $blobStore;
-       }
-
-       private function makeModule( array $messages ) {
-               $module = new ResourceLoaderTestModule( [ 'messages' => $messages ] );
-               $module->setName( 'test.blobstore' );
-               return $module;
-       }
-}
diff --git a/tests/phpunit/includes/resourceloader/ResourceLoaderClientHtmlTest.php b/tests/phpunit/includes/resourceloader/ResourceLoaderClientHtmlTest.php
deleted file mode 100644 (file)
index 03a3e24..0000000
+++ /dev/null
@@ -1,434 +0,0 @@
-<?php
-
-use Wikimedia\TestingAccessWrapper;
-
-/**
- * @group ResourceLoader
- * @covers ResourceLoaderClientHtml
- */
-class ResourceLoaderClientHtmlTest extends PHPUnit\Framework\TestCase {
-
-       use MediaWikiCoversValidator;
-
-       public function testGetData() {
-               $context = self::makeContext();
-               $context->getResourceLoader()->register( self::makeSampleModules() );
-
-               $client = new ResourceLoaderClientHtml( $context );
-               $client->setModules( [
-                       'test',
-                       'test.private',
-                       'test.shouldembed.empty',
-                       'test.shouldembed',
-                       'test.user',
-                       'test.unregistered',
-               ] );
-               $client->setModuleStyles( [
-                       'test.styles.mixed',
-                       'test.styles.user.empty',
-                       'test.styles.private',
-                       'test.styles.pure',
-                       'test.styles.shouldembed',
-                       'test.styles.deprecated',
-                       'test.unregistered.styles',
-               ] );
-
-               $expected = [
-                       'states' => [
-                               // The below are NOT queued for loading via `mw.loader.load(Array)`.
-                               // Instead we tell the client to set their state to "loading" so that
-                               // if they are needed as dependencies, the client will not try to
-                               // load them on-demand, because the server is taking care of them already.
-                               // Either:
-                               // - Embedded as inline scripts in the HTML (e.g. user-private code, and
-                               //   previews). Once that script tag is reached, the state is "loaded".
-                               // - Loaded directly from the HTML with a dedicated HTTP request (e.g.
-                               //   user scripts, which vary by a 'user' and 'version' parameter that
-                               //   the static user-agnostic startup module won't have).
-                               'test.private' => 'loading',
-                               'test.shouldembed' => 'loading',
-                               'test.user' => 'loading',
-                               // The below are known to the server to be empty scripts, or to be
-                               // synchronously loaded stylesheets. These start in the "ready" state.
-                               'test.shouldembed.empty' => 'ready',
-                               'test.styles.pure' => 'ready',
-                               'test.styles.user.empty' => 'ready',
-                               'test.styles.private' => 'ready',
-                               'test.styles.shouldembed' => 'ready',
-                               'test.styles.deprecated' => 'ready',
-                       ],
-                       'general' => [
-                               'test',
-                       ],
-                       'styles' => [
-                               'test.styles.pure',
-                               'test.styles.deprecated',
-                       ],
-                       'embed' => [
-                               'styles' => [ 'test.styles.private', 'test.styles.shouldembed' ],
-                               'general' => [
-                                       'test.private',
-                                       'test.shouldembed',
-                                       'test.user',
-                               ],
-                       ],
-                       'styleDeprecations' => [
-                               Xml::encodeJsCall(
-                                       'mw.log.warn',
-                                       [ 'This page is using the deprecated ResourceLoader module "test.styles.deprecated".
-Deprecation message.' ]
-                               )
-                       ],
-               ];
-
-               $access = TestingAccessWrapper::newFromObject( $client );
-               $this->assertEquals( $expected, $access->getData() );
-       }
-
-       public function testGetHeadHtml() {
-               $context = self::makeContext();
-               $context->getResourceLoader()->register( self::makeSampleModules() );
-
-               $client = new ResourceLoaderClientHtml( $context, [
-                       'nonce' => false,
-               ] );
-               $client->setConfig( [ 'key' => 'value' ] );
-               $client->setModules( [
-                       'test',
-                       'test.private',
-               ] );
-               $client->setModuleStyles( [
-                       'test.styles.pure',
-                       'test.styles.private',
-                       'test.styles.deprecated',
-               ] );
-               $client->setExemptStates( [
-                       'test.exempt' => 'ready',
-               ] );
-
-               // phpcs:disable Generic.Files.LineLength
-               $expected = '<script>'
-                       . 'document.documentElement.className=document.documentElement.className.replace(/(^|\s)client-nojs(\s|$)/,"$1client-js$2");'
-                       . 'RLCONF={"key":"value"};'
-                       . 'RLSTATE={"test.exempt":"ready","test.private":"loading","test.styles.pure":"ready","test.styles.private":"ready","test.styles.deprecated":"ready"};'
-                       . 'RLPAGEMODULES=["test"];'
-                       . '</script>' . "\n"
-                       . '<script>(RLQ=window.RLQ||[]).push(function(){'
-                       . 'mw.loader.implement("test.private@{blankVer}",null,{"css":[]});'
-                       . '});</script>' . "\n"
-                       . '<link rel="stylesheet" href="/w/load.php?lang=nl&amp;modules=test.styles.deprecated%2Cpure&amp;only=styles"/>' . "\n"
-                       . '<style>.private{}</style>' . "\n"
-                       . '<script async="" src="/w/load.php?lang=nl&amp;modules=startup&amp;only=scripts"></script>';
-               // phpcs:enable
-               $expected = self::expandVariables( $expected );
-
-               $this->assertSame( $expected, (string)$client->getHeadHtml() );
-       }
-
-       /**
-        * Confirm that 'target' is passed down to the startup module's load url.
-        */
-       public function testGetHeadHtmlWithTarget() {
-               $client = new ResourceLoaderClientHtml(
-                       self::makeContext(),
-                       [ 'target' => 'example' ]
-               );
-
-               // phpcs:disable Generic.Files.LineLength
-               $expected = '<script>document.documentElement.className=document.documentElement.className.replace(/(^|\s)client-nojs(\s|$)/,"$1client-js$2");</script>' . "\n"
-                       . '<script async="" src="/w/load.php?lang=nl&amp;modules=startup&amp;only=scripts&amp;target=example"></script>';
-               // phpcs:enable
-
-               $this->assertSame( $expected, (string)$client->getHeadHtml() );
-       }
-
-       /**
-        * Confirm that 'safemode' is passed down to startup.
-        */
-       public function testGetHeadHtmlWithSafemode() {
-               $client = new ResourceLoaderClientHtml(
-                       self::makeContext(),
-                       [ 'safemode' => '1' ]
-               );
-
-               // phpcs:disable Generic.Files.LineLength
-               $expected = '<script>document.documentElement.className=document.documentElement.className.replace(/(^|\s)client-nojs(\s|$)/,"$1client-js$2");</script>' . "\n"
-                       . '<script async="" src="/w/load.php?lang=nl&amp;modules=startup&amp;only=scripts&amp;safemode=1"></script>';
-               // phpcs:enable
-
-               $this->assertSame( $expected, (string)$client->getHeadHtml() );
-       }
-
-       /**
-        * Confirm that a null 'target' is the same as no target.
-        */
-       public function testGetHeadHtmlWithNullTarget() {
-               $client = new ResourceLoaderClientHtml(
-                       self::makeContext(),
-                       [ 'target' => null ]
-               );
-
-               // phpcs:disable Generic.Files.LineLength
-               $expected = '<script>document.documentElement.className=document.documentElement.className.replace(/(^|\s)client-nojs(\s|$)/,"$1client-js$2");</script>' . "\n"
-                       . '<script async="" src="/w/load.php?lang=nl&amp;modules=startup&amp;only=scripts"></script>';
-               // phpcs:enable
-
-               $this->assertSame( $expected, (string)$client->getHeadHtml() );
-       }
-
-       public function testGetBodyHtml() {
-               $context = self::makeContext();
-               $context->getResourceLoader()->register( self::makeSampleModules() );
-
-               $client = new ResourceLoaderClientHtml( $context, [ 'nonce' => false ] );
-               $client->setConfig( [ 'key' => 'value' ] );
-               $client->setModules( [
-                       'test',
-                       'test.private.bottom',
-               ] );
-               $client->setModuleStyles( [
-                       'test.styles.deprecated',
-               ] );
-               // phpcs:disable Generic.Files.LineLength
-               $expected = '<script>(RLQ=window.RLQ||[]).push(function(){'
-                       . 'mw.log.warn("This page is using the deprecated ResourceLoader module \"test.styles.deprecated\".\nDeprecation message.");'
-                       . '});</script>';
-               // phpcs:enable
-
-               $this->assertSame( $expected, (string)$client->getBodyHtml() );
-       }
-
-       public static function provideMakeLoad() {
-               // phpcs:disable Generic.Files.LineLength
-               return [
-                       [
-                               'context' => [],
-                               'modules' => [ 'test.unknown' ],
-                               'only' => ResourceLoaderModule::TYPE_STYLES,
-                               'extra' => [],
-                               'output' => '',
-                       ],
-                       [
-                               'context' => [],
-                               'modules' => [ 'test.styles.private' ],
-                               'only' => ResourceLoaderModule::TYPE_STYLES,
-                               'extra' => [],
-                               'output' => '<style>.private{}</style>',
-                       ],
-                       [
-                               'context' => [],
-                               'modules' => [ 'test.private' ],
-                               'only' => ResourceLoaderModule::TYPE_COMBINED,
-                               'extra' => [],
-                               'output' => '<script>(RLQ=window.RLQ||[]).push(function(){mw.loader.implement("test.private@{blankVer}",null,{"css":[]});});</script>',
-                       ],
-                       [
-                               'context' => [],
-                               // Eg. startup module
-                               'modules' => [ 'test.scripts.raw' ],
-                               'only' => ResourceLoaderModule::TYPE_SCRIPTS,
-                               'extra' => [],
-                               'output' => '<script async="" src="/w/load.php?lang=nl&amp;modules=test.scripts.raw&amp;only=scripts"></script>',
-                       ],
-                       [
-                               'context' => [],
-                               'modules' => [ 'test.scripts.raw' ],
-                               'only' => ResourceLoaderModule::TYPE_SCRIPTS,
-                               'extra' => [ 'sync' => '1' ],
-                               'output' => '<script src="/w/load.php?lang=nl&amp;modules=test.scripts.raw&amp;only=scripts&amp;sync=1"></script>',
-                       ],
-                       [
-                               'context' => [],
-                               'modules' => [ 'test.scripts.user' ],
-                               'only' => ResourceLoaderModule::TYPE_SCRIPTS,
-                               'extra' => [],
-                               'output' => '<script>(RLQ=window.RLQ||[]).push(function(){mw.loader.load("/w/load.php?lang=nl\u0026modules=test.scripts.user\u0026only=scripts\u0026user=Example\u0026version=0a56zyi");});</script>',
-                       ],
-                       [
-                               'context' => [],
-                               'modules' => [ 'test.user' ],
-                               'only' => ResourceLoaderModule::TYPE_COMBINED,
-                               'extra' => [],
-                               'output' => '<script>(RLQ=window.RLQ||[]).push(function(){mw.loader.load("/w/load.php?lang=nl\u0026modules=test.user\u0026user=Example\u0026version=0a56zyi");});</script>',
-                       ],
-                       [
-                               'context' => [ 'debug' => 'true' ],
-                               'modules' => [ 'test.styles.pure', 'test.styles.mixed' ],
-                               'only' => ResourceLoaderModule::TYPE_STYLES,
-                               'extra' => [],
-                               'output' => '<link rel="stylesheet" href="/w/load.php?debug=true&amp;lang=nl&amp;modules=test.styles.mixed&amp;only=styles"/>' . "\n"
-                                       . '<link rel="stylesheet" href="/w/load.php?debug=true&amp;lang=nl&amp;modules=test.styles.pure&amp;only=styles"/>',
-                       ],
-                       [
-                               'context' => [ 'debug' => 'false' ],
-                               'modules' => [ 'test.styles.pure', 'test.styles.mixed' ],
-                               'only' => ResourceLoaderModule::TYPE_STYLES,
-                               'extra' => [],
-                               'output' => '<link rel="stylesheet" href="/w/load.php?lang=nl&amp;modules=test.styles.mixed%2Cpure&amp;only=styles"/>',
-                       ],
-                       [
-                               'context' => [],
-                               'modules' => [ 'test.styles.noscript' ],
-                               'only' => ResourceLoaderModule::TYPE_STYLES,
-                               'extra' => [],
-                               'output' => '<noscript><link rel="stylesheet" href="/w/load.php?lang=nl&amp;modules=test.styles.noscript&amp;only=styles"/></noscript>',
-                       ],
-                       [
-                               'context' => [],
-                               'modules' => [ 'test.shouldembed' ],
-                               'only' => ResourceLoaderModule::TYPE_COMBINED,
-                               'extra' => [],
-                               'output' => '<script>(RLQ=window.RLQ||[]).push(function(){mw.loader.implement("test.shouldembed@09p30q0",null,{"css":[]});});</script>',
-                       ],
-                       [
-                               'context' => [],
-                               'modules' => [ 'test.styles.shouldembed' ],
-                               'only' => ResourceLoaderModule::TYPE_STYLES,
-                               'extra' => [],
-                               'output' => '<style>.shouldembed{}</style>',
-                       ],
-                       [
-                               'context' => [],
-                               'modules' => [ 'test.scripts.shouldembed' ],
-                               'only' => ResourceLoaderModule::TYPE_SCRIPTS,
-                               'extra' => [],
-                               'output' => '<script>(RLQ=window.RLQ||[]).push(function(){mw.loader.state({"test.scripts.shouldembed":"ready"});});</script>',
-                       ],
-                       [
-                               'context' => [],
-                               'modules' => [ 'test', 'test.shouldembed' ],
-                               'only' => ResourceLoaderModule::TYPE_COMBINED,
-                               'extra' => [],
-                               'output' => '<script>(RLQ=window.RLQ||[]).push(function(){mw.loader.load("/w/load.php?lang=nl\u0026modules=test");mw.loader.implement("test.shouldembed@09p30q0",null,{"css":[]});});</script>',
-                       ],
-                       [
-                               'context' => [],
-                               'modules' => [ 'test.styles.pure', 'test.styles.shouldembed' ],
-                               'only' => ResourceLoaderModule::TYPE_STYLES,
-                               'extra' => [],
-                               'output' =>
-                                       '<link rel="stylesheet" href="/w/load.php?lang=nl&amp;modules=test.styles.pure&amp;only=styles"/>' . "\n"
-                                       . '<style>.shouldembed{}</style>'
-                       ],
-                       [
-                               'context' => [],
-                               'modules' => [ 'test.ordering.a', 'test.ordering.e', 'test.ordering.b', 'test.ordering.d', 'test.ordering.c' ],
-                               'only' => ResourceLoaderModule::TYPE_STYLES,
-                               'extra' => [],
-                               'output' =>
-                                       '<link rel="stylesheet" href="/w/load.php?lang=nl&amp;modules=test.ordering.a%2Cb&amp;only=styles"/>' . "\n"
-                                       . '<style>.orderingC{}.orderingD{}</style>' . "\n"
-                                       . '<link rel="stylesheet" href="/w/load.php?lang=nl&amp;modules=test.ordering.e&amp;only=styles"/>'
-                       ],
-               ];
-               // phpcs:enable
-       }
-
-       /**
-        * @dataProvider provideMakeLoad
-        * @covers ResourceLoaderClientHtml
-        * @covers ResourceLoaderModule::getModuleContent
-        * @covers ResourceLoader
-        */
-       public function testMakeLoad(
-               array $contextQuery,
-               array $modules,
-               $type,
-               array $extraQuery,
-               $expected
-       ) {
-               $context = self::makeContext( $contextQuery );
-               $context->getResourceLoader()->register( self::makeSampleModules() );
-               $actual = ResourceLoaderClientHtml::makeLoad( $context, $modules, $type, $extraQuery, false );
-               $expected = self::expandVariables( $expected );
-               $this->assertSame( $expected, (string)$actual );
-       }
-
-       public function testGetDocumentAttributes() {
-               $client = new ResourceLoaderClientHtml( self::makeContext() );
-               $this->assertInternalType( 'array', $client->getDocumentAttributes() );
-       }
-
-       private static function expandVariables( $text ) {
-               return strtr( $text, [
-                       '{blankVer}' => ResourceLoaderTestCase::BLANK_VERSION
-               ] );
-       }
-
-       private static function makeContext( $extraQuery = [] ) {
-               $conf = new HashConfig( [
-                       'ResourceModuleSkinStyles' => [],
-                       'ResourceModules' => [],
-                       'EnableJavaScriptTest' => false,
-                       'LoadScript' => '/w/load.php',
-               ] );
-               return new ResourceLoaderContext(
-                       new ResourceLoader( $conf ),
-                       new FauxRequest( array_merge( [
-                               'lang' => 'nl',
-                               'skin' => 'fallback',
-                               'user' => 'Example',
-                               'target' => 'phpunit',
-                       ], $extraQuery ) )
-               );
-       }
-
-       private static function makeModule( array $options = [] ) {
-               return new ResourceLoaderTestModule( $options );
-       }
-
-       private static function makeSampleModules() {
-               $modules = [
-                       'test' => [],
-                       'test.private' => [ 'group' => 'private' ],
-                       'test.shouldembed.empty' => [ 'shouldEmbed' => true, 'isKnownEmpty' => true ],
-                       'test.shouldembed' => [ 'shouldEmbed' => true ],
-                       'test.user' => [ 'group' => 'user' ],
-
-                       'test.styles.pure' => [ 'type' => ResourceLoaderModule::LOAD_STYLES ],
-                       'test.styles.mixed' => [],
-                       'test.styles.noscript' => [
-                               'type' => ResourceLoaderModule::LOAD_STYLES,
-                               'group' => 'noscript',
-                       ],
-                       'test.styles.user' => [
-                               'type' => ResourceLoaderModule::LOAD_STYLES,
-                               'group' => 'user',
-                       ],
-                       'test.styles.user.empty' => [
-                               'type' => ResourceLoaderModule::LOAD_STYLES,
-                               'group' => 'user',
-                               'isKnownEmpty' => true,
-                       ],
-                       'test.styles.private' => [
-                               'type' => ResourceLoaderModule::LOAD_STYLES,
-                               'group' => 'private',
-                               'styles' => '.private{}',
-                       ],
-                       'test.styles.shouldembed' => [
-                               'type' => ResourceLoaderModule::LOAD_STYLES,
-                               'shouldEmbed' => true,
-                               'styles' => '.shouldembed{}',
-                       ],
-                       'test.styles.deprecated' => [
-                               'type' => ResourceLoaderModule::LOAD_STYLES,
-                               'deprecated' => 'Deprecation message.',
-                       ],
-
-                       'test.scripts' => [],
-                       'test.scripts.user' => [ 'group' => 'user' ],
-                       'test.scripts.user.empty' => [ 'group' => 'user', 'isKnownEmpty' => true ],
-                       'test.scripts.raw' => [ 'isRaw' => true ],
-                       'test.scripts.shouldembed' => [ 'shouldEmbed' => true ],
-
-                       'test.ordering.a' => [ 'shouldEmbed' => false ],
-                       'test.ordering.b' => [ 'shouldEmbed' => false ],
-                       'test.ordering.c' => [ 'shouldEmbed' => true, 'styles' => '.orderingC{}' ],
-                       'test.ordering.d' => [ 'shouldEmbed' => true, 'styles' => '.orderingD{}' ],
-                       'test.ordering.e' => [ 'shouldEmbed' => false ],
-               ];
-               return array_map( function ( $options ) {
-                       return self::makeModule( $options );
-               }, $modules );
-       }
-}
diff --git a/tests/phpunit/includes/resourceloader/ResourceLoaderContextTest.php b/tests/phpunit/includes/resourceloader/ResourceLoaderContextTest.php
deleted file mode 100644 (file)
index 2ec8ea9..0000000
+++ /dev/null
@@ -1,122 +0,0 @@
-<?php
-
-/**
- * See also:
- * - ResourceLoaderImageModuleTest::testContext
- *
- * @group ResourceLoader
- * @covers ResourceLoaderContext
- */
-class ResourceLoaderContextTest extends PHPUnit\Framework\TestCase {
-
-       use MediaWikiCoversValidator;
-
-       protected static function getResourceLoader() {
-               return new EmptyResourceLoader( new HashConfig( [
-                       'ResourceLoaderDebug' => false,
-                       'LoadScript' => '/w/load.php',
-                       // For ResourceLoader::register()
-                       'ResourceModuleSkinStyles' => [],
-               ] ) );
-       }
-
-       public function testEmpty() {
-               $ctx = new ResourceLoaderContext( $this->getResourceLoader(), new FauxRequest( [] ) );
-
-               // Request parameters
-               $this->assertEquals( [], $ctx->getModules() );
-               $this->assertEquals( 'qqx', $ctx->getLanguage() );
-               $this->assertEquals( false, $ctx->getDebug() );
-               $this->assertEquals( null, $ctx->getOnly() );
-               $this->assertEquals( 'fallback', $ctx->getSkin() );
-               $this->assertEquals( null, $ctx->getUser() );
-               $this->assertNull( $ctx->getContentOverrideCallback() );
-
-               // Misc
-               $this->assertEquals( 'ltr', $ctx->getDirection() );
-               $this->assertEquals( 'qqx|fallback||||||||', $ctx->getHash() );
-               $this->assertInstanceOf( User::class, $ctx->getUserObj() );
-       }
-
-       public function testDummy() {
-               $this->assertInstanceOf(
-                       ResourceLoaderContext::class,
-                       ResourceLoaderContext::newDummyContext()
-               );
-       }
-
-       public function testAccessors() {
-               $ctx = new ResourceLoaderContext( $this->getResourceLoader(), new FauxRequest( [] ) );
-               $this->assertInstanceOf( ResourceLoader::class, $ctx->getResourceLoader() );
-               $this->assertInstanceOf( Config::class, $ctx->getConfig() );
-               $this->assertInstanceOf( WebRequest::class, $ctx->getRequest() );
-               $this->assertInstanceOf( Psr\Log\LoggerInterface::class, $ctx->getLogger() );
-       }
-
-       public function testTypicalRequest() {
-               $ctx = new ResourceLoaderContext( $this->getResourceLoader(), new FauxRequest( [
-                       'debug' => 'false',
-                       'lang' => 'zh',
-                       'modules' => 'foo|foo.quux,baz,bar|baz.quux',
-                       'only' => 'styles',
-                       'skin' => 'fallback',
-               ] ) );
-
-               // Request parameters
-               $this->assertEquals(
-                       $ctx->getModules(),
-                       [ 'foo', 'foo.quux', 'foo.baz', 'foo.bar', 'baz.quux' ]
-               );
-               $this->assertEquals( false, $ctx->getDebug() );
-               $this->assertEquals( 'zh', $ctx->getLanguage() );
-               $this->assertEquals( 'styles', $ctx->getOnly() );
-               $this->assertEquals( 'fallback', $ctx->getSkin() );
-               $this->assertEquals( null, $ctx->getUser() );
-
-               // Misc
-               $this->assertEquals( 'ltr', $ctx->getDirection() );
-               $this->assertEquals( 'zh|fallback|||styles|||||', $ctx->getHash() );
-       }
-
-       public function testShouldInclude() {
-               $ctx = new ResourceLoaderContext( $this->getResourceLoader(), new FauxRequest( [] ) );
-               $this->assertTrue( $ctx->shouldIncludeScripts(), 'Scripts in combined' );
-               $this->assertTrue( $ctx->shouldIncludeStyles(), 'Styles in combined' );
-               $this->assertTrue( $ctx->shouldIncludeMessages(), 'Messages in combined' );
-
-               $ctx = new ResourceLoaderContext( $this->getResourceLoader(), new FauxRequest( [
-                       'only' => 'styles'
-               ] ) );
-               $this->assertFalse( $ctx->shouldIncludeScripts(), 'Scripts not in styles-only' );
-               $this->assertTrue( $ctx->shouldIncludeStyles(), 'Styles in styles-only' );
-               $this->assertFalse( $ctx->shouldIncludeMessages(), 'Messages not in styles-only' );
-
-               $ctx = new ResourceLoaderContext( $this->getResourceLoader(), new FauxRequest( [
-                       'only' => 'scripts'
-               ] ) );
-               $this->assertTrue( $ctx->shouldIncludeScripts(), 'Scripts in scripts-only' );
-               $this->assertFalse( $ctx->shouldIncludeStyles(), 'Styles not in scripts-only' );
-               $this->assertFalse( $ctx->shouldIncludeMessages(), 'Messages not in scripts-only' );
-       }
-
-       public function testGetUser() {
-               $ctx = new ResourceLoaderContext( $this->getResourceLoader(), new FauxRequest( [] ) );
-               $this->assertSame( null, $ctx->getUser() );
-               $this->assertTrue( $ctx->getUserObj()->isAnon() );
-
-               $ctx = new ResourceLoaderContext( $this->getResourceLoader(), new FauxRequest( [
-                       'user' => 'Example'
-               ] ) );
-               $this->assertSame( 'Example', $ctx->getUser() );
-               $this->assertEquals( 'Example', $ctx->getUserObj()->getName() );
-       }
-
-       public function testMsg() {
-               $ctx = new ResourceLoaderContext( $this->getResourceLoader(), new FauxRequest( [
-                       'lang' => 'en'
-               ] ) );
-               $msg = $ctx->msg( 'mainpage' );
-               $this->assertInstanceOf( Message::class, $msg );
-               $this->assertSame( 'Main Page', $msg->useDatabase( false )->plain() );
-       }
-}
diff --git a/tests/phpunit/includes/search/SearchIndexFieldTest.php b/tests/phpunit/includes/search/SearchIndexFieldTest.php
deleted file mode 100644 (file)
index 8b4119e..0000000
+++ /dev/null
@@ -1,56 +0,0 @@
-<?php
-
-/**
- * @group Search
- * @covers SearchIndexFieldDefinition
- */
-class SearchIndexFieldTest extends MediaWikiTestCase {
-
-       public function getMergeCases() {
-               return [
-                       [ 0, 'test', 0, 'test', true ],
-                       [ SearchIndexField::INDEX_TYPE_NESTED, 'test',
-                               SearchIndexField::INDEX_TYPE_NESTED, 'test', false ],
-                       [ 0, 'test', 0, 'test2', true ],
-                       [ 0, 'test', 1, 'test', false ],
-               ];
-       }
-
-       /**
-        * @dataProvider getMergeCases
-        * @param int $t1
-        * @param string $n1
-        * @param int $t2
-        * @param string $n2
-        * @param bool $result
-        */
-       public function testMerge( $t1, $n1, $t2, $n2, $result ) {
-               $field1 =
-                       $this->getMockBuilder( SearchIndexFieldDefinition::class )
-                               ->setMethods( [ 'getMapping' ] )
-                               ->setConstructorArgs( [ $n1, $t1 ] )
-                               ->getMock();
-               $field2 =
-                       $this->getMockBuilder( SearchIndexFieldDefinition::class )
-                               ->setMethods( [ 'getMapping' ] )
-                               ->setConstructorArgs( [ $n2, $t2 ] )
-                               ->getMock();
-
-               if ( $result ) {
-                       $this->assertNotFalse( $field1->merge( $field2 ) );
-               } else {
-                       $this->assertFalse( $field1->merge( $field2 ) );
-               }
-
-               $field1->setFlag( 0xFF );
-               $this->assertFalse( $field1->merge( $field2 ) );
-
-               $field1->setMergeCallback(
-                       function ( $a, $b ) {
-                               return "test";
-                       }
-               );
-               $this->assertEquals( "test", $field1->merge( $field2 ) );
-       }
-
-}
diff --git a/tests/phpunit/includes/search/SearchSuggestionSetTest.php b/tests/phpunit/includes/search/SearchSuggestionSetTest.php
deleted file mode 100644 (file)
index 02fa5e9..0000000
+++ /dev/null
@@ -1,111 +0,0 @@
-<?php
-
-/**
- * Test for filter utilities.
- *
- * 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; either version 2 of the License, or
- * (at your option) any later version.
- *
- * 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.
- * http://www.gnu.org/copyleft/gpl.html
- */
-
-class SearchSuggestionSetTest extends \PHPUnit\Framework\TestCase {
-       /**
-        * Test that adding a new suggestion at the end
-        * will keep proper score ordering
-        * @covers SearchSuggestionSet::append
-        */
-       public function testAppend() {
-               $set = SearchSuggestionSet::emptySuggestionSet();
-               $this->assertEquals( 0, $set->getSize() );
-               $set->append( new SearchSuggestion( 3 ) );
-               $this->assertEquals( 3, $set->getWorstScore() );
-               $this->assertEquals( 3, $set->getBestScore() );
-
-               $suggestion = new SearchSuggestion( 4 );
-               $set->append( $suggestion );
-               $this->assertEquals( 2, $set->getWorstScore() );
-               $this->assertEquals( 3, $set->getBestScore() );
-               $this->assertEquals( 2, $suggestion->getScore() );
-
-               $suggestion = new SearchSuggestion( 2 );
-               $set->append( $suggestion );
-               $this->assertEquals( 1, $set->getWorstScore() );
-               $this->assertEquals( 3, $set->getBestScore() );
-               $this->assertEquals( 1, $suggestion->getScore() );
-
-               $scores = $set->map( function ( $s ) {
-                       return $s->getScore();
-               } );
-               $sorted = $scores;
-               asort( $sorted );
-               $this->assertEquals( $sorted, $scores );
-       }
-
-       /**
-        * Test that adding a new best suggestion will keep proper score
-        * ordering
-        * @covers SearchSuggestionSet::getWorstScore
-        * @covers SearchSuggestionSet::getBestScore
-        * @covers SearchSuggestionSet::prepend
-        */
-       public function testInsertBest() {
-               $set = SearchSuggestionSet::emptySuggestionSet();
-               $this->assertEquals( 0, $set->getSize() );
-               $set->prepend( new SearchSuggestion( 3 ) );
-               $this->assertEquals( 3, $set->getWorstScore() );
-               $this->assertEquals( 3, $set->getBestScore() );
-
-               $suggestion = new SearchSuggestion( 4 );
-               $set->prepend( $suggestion );
-               $this->assertEquals( 3, $set->getWorstScore() );
-               $this->assertEquals( 4, $set->getBestScore() );
-               $this->assertEquals( 4, $suggestion->getScore() );
-
-               $suggestion = new SearchSuggestion( 0 );
-               $set->prepend( $suggestion );
-               $this->assertEquals( 3, $set->getWorstScore() );
-               $this->assertEquals( 5, $set->getBestScore() );
-               $this->assertEquals( 5, $suggestion->getScore() );
-
-               $suggestion = new SearchSuggestion( 2 );
-               $set->prepend( $suggestion );
-               $this->assertEquals( 3, $set->getWorstScore() );
-               $this->assertEquals( 6, $set->getBestScore() );
-               $this->assertEquals( 6, $suggestion->getScore() );
-
-               $scores = $set->map( function ( $s ) {
-                       return $s->getScore();
-               } );
-               $sorted = $scores;
-               asort( $sorted );
-               $this->assertEquals( $sorted, $scores );
-       }
-
-       /**
-        * @covers SearchSuggestionSet::shrink
-        */
-       public function testShrink() {
-               $set = SearchSuggestionSet::emptySuggestionSet();
-               for ( $i = 0; $i < 100; $i++ ) {
-                       $set->append( new SearchSuggestion( 0 ) );
-               }
-               $set->shrink( 10 );
-               $this->assertEquals( 10, $set->getSize() );
-
-               $set->shrink( 0 );
-               $this->assertEquals( 0, $set->getSize() );
-       }
-
-       // TODO: test for fromTitles
-}
diff --git a/tests/phpunit/includes/session/MetadataMergeExceptionTest.php b/tests/phpunit/includes/session/MetadataMergeExceptionTest.php
deleted file mode 100644 (file)
index 8cb4302..0000000
+++ /dev/null
@@ -1,30 +0,0 @@
-<?php
-
-namespace MediaWiki\Session;
-
-use MediaWikiTestCase;
-
-/**
- * @group Session
- * @covers MediaWiki\Session\MetadataMergeException
- */
-class MetadataMergeExceptionTest extends MediaWikiTestCase {
-
-       public function testBasics() {
-               $data = [ 'foo' => 'bar' ];
-
-               $ex = new MetadataMergeException();
-               $this->assertInstanceOf( \UnexpectedValueException::class, $ex );
-               $this->assertSame( [], $ex->getContext() );
-
-               $ex2 = new MetadataMergeException( 'Message', 42, $ex, $data );
-               $this->assertSame( 'Message', $ex2->getMessage() );
-               $this->assertSame( 42, $ex2->getCode() );
-               $this->assertSame( $ex, $ex2->getPrevious() );
-               $this->assertSame( $data, $ex2->getContext() );
-
-               $ex->setContext( $data );
-               $this->assertSame( $data, $ex->getContext() );
-       }
-
-}
diff --git a/tests/phpunit/includes/session/SessionIdTest.php b/tests/phpunit/includes/session/SessionIdTest.php
deleted file mode 100644 (file)
index 2b06d97..0000000
+++ /dev/null
@@ -1,22 +0,0 @@
-<?php
-
-namespace MediaWiki\Session;
-
-use MediaWikiTestCase;
-
-/**
- * @group Session
- * @covers MediaWiki\Session\SessionId
- */
-class SessionIdTest extends MediaWikiTestCase {
-
-       public function testEverything() {
-               $id = new SessionId( 'foo' );
-               $this->assertSame( 'foo', $id->getId() );
-               $this->assertSame( 'foo', (string)$id );
-               $id->setId( 'bar' );
-               $this->assertSame( 'bar', $id->getId() );
-               $this->assertSame( 'bar', (string)$id );
-       }
-
-}
diff --git a/tests/phpunit/includes/session/SessionInfoTest.php b/tests/phpunit/includes/session/SessionInfoTest.php
deleted file mode 100644 (file)
index 8f7b2a6..0000000
+++ /dev/null
@@ -1,356 +0,0 @@
-<?php
-
-namespace MediaWiki\Session;
-
-use MediaWikiTestCase;
-
-/**
- * @group Session
- * @group Database
- * @covers MediaWiki\Session\SessionInfo
- */
-class SessionInfoTest extends MediaWikiTestCase {
-
-       public function testBasics() {
-               $anonInfo = UserInfo::newAnonymous();
-               $userInfo = UserInfo::newFromName( 'UTSysop', true );
-               $unverifiedUserInfo = UserInfo::newFromName( 'UTSysop', false );
-
-               try {
-                       new SessionInfo( SessionInfo::MIN_PRIORITY - 1, [] );
-                       $this->fail( 'Expected exception not thrown', 'priority < min' );
-               } catch ( \InvalidArgumentException $ex ) {
-                       $this->assertSame( 'Invalid priority', $ex->getMessage(), 'priority < min' );
-               }
-
-               try {
-                       new SessionInfo( SessionInfo::MAX_PRIORITY + 1, [] );
-                       $this->fail( 'Expected exception not thrown', 'priority > max' );
-               } catch ( \InvalidArgumentException $ex ) {
-                       $this->assertSame( 'Invalid priority', $ex->getMessage(), 'priority > max' );
-               }
-
-               try {
-                       new SessionInfo( SessionInfo::MIN_PRIORITY, [ 'id' => 'ABC?' ] );
-                       $this->fail( 'Expected exception not thrown', 'bad session ID' );
-               } catch ( \InvalidArgumentException $ex ) {
-                       $this->assertSame( 'Invalid session ID', $ex->getMessage(), 'bad session ID' );
-               }
-
-               try {
-                       new SessionInfo( SessionInfo::MIN_PRIORITY, [ 'userInfo' => new \stdClass ] );
-                       $this->fail( 'Expected exception not thrown', 'bad userInfo' );
-               } catch ( \InvalidArgumentException $ex ) {
-                       $this->assertSame( 'Invalid userInfo', $ex->getMessage(), 'bad userInfo' );
-               }
-
-               try {
-                       new SessionInfo( SessionInfo::MIN_PRIORITY, [] );
-                       $this->fail( 'Expected exception not thrown', 'no provider, no id' );
-               } catch ( \InvalidArgumentException $ex ) {
-                       $this->assertSame( 'Must supply an ID when no provider is given', $ex->getMessage(),
-                               'no provider, no id' );
-               }
-
-               try {
-                       new SessionInfo( SessionInfo::MIN_PRIORITY, [ 'copyFrom' => new \stdClass ] );
-                       $this->fail( 'Expected exception not thrown', 'bad copyFrom' );
-               } catch ( \InvalidArgumentException $ex ) {
-                       $this->assertSame( 'Invalid copyFrom', $ex->getMessage(),
-                               'bad copyFrom' );
-               }
-
-               $manager = new SessionManager();
-               $provider = $this->getMockBuilder( SessionProvider::class )
-                       ->setMethods( [ 'persistsSessionId', 'canChangeUser', '__toString' ] )
-                       ->getMockForAbstractClass();
-               $provider->setManager( $manager );
-               $provider->expects( $this->any() )->method( 'persistsSessionId' )
-                       ->will( $this->returnValue( true ) );
-               $provider->expects( $this->any() )->method( 'canChangeUser' )
-                       ->will( $this->returnValue( true ) );
-               $provider->expects( $this->any() )->method( '__toString' )
-                       ->will( $this->returnValue( 'Mock' ) );
-
-               $provider2 = $this->getMockBuilder( SessionProvider::class )
-                       ->setMethods( [ 'persistsSessionId', 'canChangeUser', '__toString' ] )
-                       ->getMockForAbstractClass();
-               $provider2->setManager( $manager );
-               $provider2->expects( $this->any() )->method( 'persistsSessionId' )
-                       ->will( $this->returnValue( true ) );
-               $provider2->expects( $this->any() )->method( 'canChangeUser' )
-                       ->will( $this->returnValue( true ) );
-               $provider2->expects( $this->any() )->method( '__toString' )
-                       ->will( $this->returnValue( 'Mock2' ) );
-
-               try {
-                       new SessionInfo( SessionInfo::MIN_PRIORITY, [
-                               'provider' => $provider,
-                               'userInfo' => $anonInfo,
-                               'metadata' => 'foo',
-                       ] );
-                       $this->fail( 'Expected exception not thrown', 'bad metadata' );
-               } catch ( \InvalidArgumentException $ex ) {
-                       $this->assertSame( 'Invalid metadata', $ex->getMessage(), 'bad metadata' );
-               }
-
-               $info = new SessionInfo( SessionInfo::MIN_PRIORITY + 5, [
-                       'provider' => $provider,
-                       'userInfo' => $anonInfo
-               ] );
-               $this->assertSame( $provider, $info->getProvider() );
-               $this->assertNotNull( $info->getId() );
-               $this->assertSame( SessionInfo::MIN_PRIORITY + 5, $info->getPriority() );
-               $this->assertSame( $anonInfo, $info->getUserInfo() );
-               $this->assertTrue( $info->isIdSafe() );
-               $this->assertFalse( $info->forceUse() );
-               $this->assertFalse( $info->wasPersisted() );
-               $this->assertFalse( $info->wasRemembered() );
-               $this->assertFalse( $info->forceHTTPS() );
-               $this->assertNull( $info->getProviderMetadata() );
-
-               $info = new SessionInfo( SessionInfo::MIN_PRIORITY + 5, [
-                       'provider' => $provider,
-                       'userInfo' => $unverifiedUserInfo,
-                       'metadata' => [ 'Foo' ],
-               ] );
-               $this->assertSame( $provider, $info->getProvider() );
-               $this->assertNotNull( $info->getId() );
-               $this->assertSame( SessionInfo::MIN_PRIORITY + 5, $info->getPriority() );
-               $this->assertSame( $unverifiedUserInfo, $info->getUserInfo() );
-               $this->assertTrue( $info->isIdSafe() );
-               $this->assertFalse( $info->forceUse() );
-               $this->assertFalse( $info->wasPersisted() );
-               $this->assertFalse( $info->wasRemembered() );
-               $this->assertFalse( $info->forceHTTPS() );
-               $this->assertSame( [ 'Foo' ], $info->getProviderMetadata() );
-
-               $info = new SessionInfo( SessionInfo::MIN_PRIORITY + 5, [
-                       'provider' => $provider,
-                       'userInfo' => $userInfo
-               ] );
-               $this->assertSame( $provider, $info->getProvider() );
-               $this->assertNotNull( $info->getId() );
-               $this->assertSame( SessionInfo::MIN_PRIORITY + 5, $info->getPriority() );
-               $this->assertSame( $userInfo, $info->getUserInfo() );
-               $this->assertTrue( $info->isIdSafe() );
-               $this->assertFalse( $info->forceUse() );
-               $this->assertFalse( $info->wasPersisted() );
-               $this->assertTrue( $info->wasRemembered() );
-               $this->assertFalse( $info->forceHTTPS() );
-               $this->assertNull( $info->getProviderMetadata() );
-
-               $id = $manager->generateSessionId();
-
-               $info = new SessionInfo( SessionInfo::MIN_PRIORITY + 5, [
-                       'provider' => $provider,
-                       'id' => $id,
-                       'persisted' => true,
-                       'userInfo' => $anonInfo
-               ] );
-               $this->assertSame( $provider, $info->getProvider() );
-               $this->assertSame( $id, $info->getId() );
-               $this->assertSame( SessionInfo::MIN_PRIORITY + 5, $info->getPriority() );
-               $this->assertSame( $anonInfo, $info->getUserInfo() );
-               $this->assertFalse( $info->isIdSafe() );
-               $this->assertFalse( $info->forceUse() );
-               $this->assertTrue( $info->wasPersisted() );
-               $this->assertFalse( $info->wasRemembered() );
-               $this->assertFalse( $info->forceHTTPS() );
-               $this->assertNull( $info->getProviderMetadata() );
-
-               $info = new SessionInfo( SessionInfo::MIN_PRIORITY + 5, [
-                       'provider' => $provider,
-                       'id' => $id,
-                       'userInfo' => $userInfo
-               ] );
-               $this->assertSame( $provider, $info->getProvider() );
-               $this->assertSame( $id, $info->getId() );
-               $this->assertSame( SessionInfo::MIN_PRIORITY + 5, $info->getPriority() );
-               $this->assertSame( $userInfo, $info->getUserInfo() );
-               $this->assertFalse( $info->isIdSafe() );
-               $this->assertFalse( $info->forceUse() );
-               $this->assertFalse( $info->wasPersisted() );
-               $this->assertTrue( $info->wasRemembered() );
-               $this->assertFalse( $info->forceHTTPS() );
-               $this->assertNull( $info->getProviderMetadata() );
-
-               $info = new SessionInfo( SessionInfo::MIN_PRIORITY + 5, [
-                       'id' => $id,
-                       'persisted' => true,
-                       'userInfo' => $userInfo,
-                       'metadata' => [ 'Foo' ],
-               ] );
-               $this->assertSame( $id, $info->getId() );
-               $this->assertSame( SessionInfo::MIN_PRIORITY + 5, $info->getPriority() );
-               $this->assertSame( $userInfo, $info->getUserInfo() );
-               $this->assertFalse( $info->isIdSafe() );
-               $this->assertFalse( $info->forceUse() );
-               $this->assertTrue( $info->wasPersisted() );
-               $this->assertFalse( $info->wasRemembered() );
-               $this->assertFalse( $info->forceHTTPS() );
-               $this->assertNull( $info->getProviderMetadata() );
-
-               $info = new SessionInfo( SessionInfo::MIN_PRIORITY + 5, [
-                       'id' => $id,
-                       'remembered' => true,
-                       'userInfo' => $userInfo,
-               ] );
-               $this->assertFalse( $info->wasRemembered(), 'no provider' );
-
-               $info = new SessionInfo( SessionInfo::MIN_PRIORITY + 5, [
-                       'provider' => $provider,
-                       'id' => $id,
-                       'remembered' => true,
-               ] );
-               $this->assertFalse( $info->wasRemembered(), 'no user' );
-
-               $info = new SessionInfo( SessionInfo::MIN_PRIORITY + 5, [
-                       'provider' => $provider,
-                       'id' => $id,
-                       'remembered' => true,
-                       'userInfo' => $anonInfo,
-               ] );
-               $this->assertFalse( $info->wasRemembered(), 'anonymous user' );
-
-               $info = new SessionInfo( SessionInfo::MIN_PRIORITY + 5, [
-                       'provider' => $provider,
-                       'id' => $id,
-                       'remembered' => true,
-                       'userInfo' => $unverifiedUserInfo,
-               ] );
-               $this->assertFalse( $info->wasRemembered(), 'unverified user' );
-
-               $info = new SessionInfo( SessionInfo::MIN_PRIORITY + 5, [
-                       'provider' => $provider,
-                       'id' => $id,
-                       'remembered' => false,
-                       'userInfo' => $userInfo,
-               ] );
-               $this->assertFalse( $info->wasRemembered(), 'specific override' );
-
-               $info = new SessionInfo( SessionInfo::MIN_PRIORITY + 5, [
-                       'id' => $id,
-                       'idIsSafe' => true,
-               ] );
-               $this->assertSame( $id, $info->getId() );
-               $this->assertSame( SessionInfo::MIN_PRIORITY + 5, $info->getPriority() );
-               $this->assertTrue( $info->isIdSafe() );
-
-               $info = new SessionInfo( SessionInfo::MIN_PRIORITY + 5, [
-                       'id' => $id,
-                       'forceUse' => true,
-               ] );
-               $this->assertFalse( $info->forceUse(), 'no provider' );
-
-               $info = new SessionInfo( SessionInfo::MIN_PRIORITY + 5, [
-                       'provider' => $provider,
-                       'forceUse' => true,
-               ] );
-               $this->assertFalse( $info->forceUse(), 'no id' );
-
-               $info = new SessionInfo( SessionInfo::MIN_PRIORITY + 5, [
-                       'provider' => $provider,
-                       'id' => $id,
-                       'forceUse' => true,
-               ] );
-               $this->assertTrue( $info->forceUse(), 'correct use' );
-
-               $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [
-                       'id' => $id,
-                       'forceHTTPS' => 1,
-               ] );
-               $this->assertTrue( $info->forceHTTPS() );
-
-               $fromInfo = new SessionInfo( SessionInfo::MIN_PRIORITY, [
-                       'id' => $id . 'A',
-                       'provider' => $provider,
-                       'userInfo' => $userInfo,
-                       'idIsSafe' => true,
-                       'forceUse' => true,
-                       'persisted' => true,
-                       'remembered' => true,
-                       'forceHTTPS' => true,
-                       'metadata' => [ 'foo!' ],
-               ] );
-               $info = new SessionInfo( SessionInfo::MIN_PRIORITY + 4, [
-                       'copyFrom' => $fromInfo,
-               ] );
-               $this->assertSame( $id . 'A', $info->getId() );
-               $this->assertSame( SessionInfo::MIN_PRIORITY + 4, $info->getPriority() );
-               $this->assertSame( $provider, $info->getProvider() );
-               $this->assertSame( $userInfo, $info->getUserInfo() );
-               $this->assertTrue( $info->isIdSafe() );
-               $this->assertTrue( $info->forceUse() );
-               $this->assertTrue( $info->wasPersisted() );
-               $this->assertTrue( $info->wasRemembered() );
-               $this->assertTrue( $info->forceHTTPS() );
-               $this->assertSame( [ 'foo!' ], $info->getProviderMetadata() );
-
-               $info = new SessionInfo( SessionInfo::MIN_PRIORITY + 4, [
-                       'id' => $id . 'X',
-                       'provider' => $provider2,
-                       'userInfo' => $unverifiedUserInfo,
-                       'idIsSafe' => false,
-                       'forceUse' => false,
-                       'persisted' => false,
-                       'remembered' => false,
-                       'forceHTTPS' => false,
-                       'metadata' => null,
-                       'copyFrom' => $fromInfo,
-               ] );
-               $this->assertSame( $id . 'X', $info->getId() );
-               $this->assertSame( SessionInfo::MIN_PRIORITY + 4, $info->getPriority() );
-               $this->assertSame( $provider2, $info->getProvider() );
-               $this->assertSame( $unverifiedUserInfo, $info->getUserInfo() );
-               $this->assertFalse( $info->isIdSafe() );
-               $this->assertFalse( $info->forceUse() );
-               $this->assertFalse( $info->wasPersisted() );
-               $this->assertFalse( $info->wasRemembered() );
-               $this->assertFalse( $info->forceHTTPS() );
-               $this->assertNull( $info->getProviderMetadata() );
-
-               $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [
-                       'id' => $id,
-               ] );
-               $this->assertSame(
-                       '[' . SessionInfo::MIN_PRIORITY . "]null<null>$id",
-                       (string)$info,
-                       'toString'
-               );
-
-               $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [
-                       'provider' => $provider,
-                       'id' => $id,
-                       'persisted' => true,
-                       'userInfo' => $userInfo
-               ] );
-               $this->assertSame(
-                       '[' . SessionInfo::MIN_PRIORITY . "]Mock<+:{$userInfo->getId()}:UTSysop>$id",
-                       (string)$info,
-                       'toString'
-               );
-
-               $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [
-                       'provider' => $provider,
-                       'id' => $id,
-                       'persisted' => true,
-                       'userInfo' => $unverifiedUserInfo
-               ] );
-               $this->assertSame(
-                       '[' . SessionInfo::MIN_PRIORITY . "]Mock<-:{$userInfo->getId()}:UTSysop>$id",
-                       (string)$info,
-                       'toString'
-               );
-       }
-
-       public function testCompare() {
-               $id = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
-               $info1 = new SessionInfo( SessionInfo::MIN_PRIORITY + 1, [ 'id' => $id ] );
-               $info2 = new SessionInfo( SessionInfo::MIN_PRIORITY + 2, [ 'id' => $id ] );
-
-               $this->assertTrue( SessionInfo::compare( $info1, $info2 ) < 0, '<' );
-               $this->assertTrue( SessionInfo::compare( $info2, $info1 ) > 0, '>' );
-               $this->assertTrue( SessionInfo::compare( $info1, $info1 ) === 0, '==' );
-       }
-}
diff --git a/tests/phpunit/includes/session/SessionProviderTest.php b/tests/phpunit/includes/session/SessionProviderTest.php
deleted file mode 100644 (file)
index 6ff6a97..0000000
+++ /dev/null
@@ -1,206 +0,0 @@
-<?php
-
-namespace MediaWiki\Session;
-
-use MediaWikiTestCase;
-use Wikimedia\TestingAccessWrapper;
-
-/**
- * @group Session
- * @group Database
- * @covers MediaWiki\Session\SessionProvider
- */
-class SessionProviderTest extends MediaWikiTestCase {
-
-       public function testBasics() {
-               $manager = new SessionManager();
-               $logger = new \TestLogger();
-               $config = new \HashConfig();
-
-               $provider = $this->getMockForAbstractClass( SessionProvider::class );
-               $priv = TestingAccessWrapper::newFromObject( $provider );
-
-               $provider->setConfig( $config );
-               $this->assertSame( $config, $priv->config );
-               $provider->setLogger( $logger );
-               $this->assertSame( $logger, $priv->logger );
-               $provider->setManager( $manager );
-               $this->assertSame( $manager, $priv->manager );
-               $this->assertSame( $manager, $provider->getManager() );
-
-               $provider->invalidateSessionsForUser( new \User );
-
-               $this->assertSame( [], $provider->getVaryHeaders() );
-               $this->assertSame( [], $provider->getVaryCookies() );
-               $this->assertSame( null, $provider->suggestLoginUsername( new \FauxRequest ) );
-
-               $this->assertSame( get_class( $provider ), (string)$provider );
-
-               $this->assertNull( $provider->getRememberUserDuration() );
-
-               $this->assertNull( $provider->whyNoSession() );
-
-               $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [
-                       'id' => 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
-                       'provider' => $provider,
-               ] );
-               $metadata = [ 'foo' ];
-               $this->assertTrue( $provider->refreshSessionInfo( $info, new \FauxRequest, $metadata ) );
-               $this->assertSame( [ 'foo' ], $metadata );
-       }
-
-       /**
-        * @dataProvider provideNewSessionInfo
-        * @param bool $persistId Return value for ->persistsSessionId()
-        * @param bool $persistUser Return value for ->persistsSessionUser()
-        * @param bool $ok Whether a SessionInfo is provided
-        */
-       public function testNewSessionInfo( $persistId, $persistUser, $ok ) {
-               $manager = new SessionManager();
-
-               $provider = $this->getMockBuilder( SessionProvider::class )
-                       ->setMethods( [ 'canChangeUser', 'persistsSessionId' ] )
-                       ->getMockForAbstractClass();
-               $provider->expects( $this->any() )->method( 'persistsSessionId' )
-                       ->will( $this->returnValue( $persistId ) );
-               $provider->expects( $this->any() )->method( 'canChangeUser' )
-                       ->will( $this->returnValue( $persistUser ) );
-               $provider->setManager( $manager );
-
-               if ( $ok ) {
-                       $info = $provider->newSessionInfo();
-                       $this->assertNotNull( $info );
-                       $this->assertFalse( $info->wasPersisted() );
-                       $this->assertTrue( $info->isIdSafe() );
-
-                       $id = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
-                       $info = $provider->newSessionInfo( $id );
-                       $this->assertNotNull( $info );
-                       $this->assertSame( $id, $info->getId() );
-                       $this->assertFalse( $info->wasPersisted() );
-                       $this->assertTrue( $info->isIdSafe() );
-               } else {
-                       $this->assertNull( $provider->newSessionInfo() );
-               }
-       }
-
-       public function testMergeMetadata() {
-               $provider = $this->getMockBuilder( SessionProvider::class )
-                       ->getMockForAbstractClass();
-
-               try {
-                       $provider->mergeMetadata(
-                               [ 'foo' => 1, 'baz' => 3 ],
-                               [ 'bar' => 2, 'baz' => '3' ]
-                       );
-                       $this->fail( 'Expected exception not thrown' );
-               } catch ( MetadataMergeException $ex ) {
-                       $this->assertSame( 'Key "baz" changed', $ex->getMessage() );
-                       $this->assertSame(
-                               [ 'old_value' => 3, 'new_value' => '3' ], $ex->getContext() );
-               }
-
-               $res = $provider->mergeMetadata(
-                       [ 'foo' => 1, 'baz' => 3 ],
-                       [ 'bar' => 2, 'baz' => 3 ]
-               );
-               $this->assertSame( [ 'bar' => 2, 'baz' => 3 ], $res );
-       }
-
-       public static function provideNewSessionInfo() {
-               return [
-                       [ false, false, false ],
-                       [ true, false, false ],
-                       [ false, true, false ],
-                       [ true, true, true ],
-               ];
-       }
-
-       public function testImmutableSessions() {
-               $provider = $this->getMockBuilder( SessionProvider::class )
-                       ->setMethods( [ 'canChangeUser', 'persistsSessionId' ] )
-                       ->getMockForAbstractClass();
-               $provider->expects( $this->any() )->method( 'canChangeUser' )
-                       ->will( $this->returnValue( true ) );
-               $provider->preventSessionsForUser( 'Foo' );
-
-               $provider = $this->getMockBuilder( SessionProvider::class )
-                       ->setMethods( [ 'canChangeUser', 'persistsSessionId' ] )
-                       ->getMockForAbstractClass();
-               $provider->expects( $this->any() )->method( 'canChangeUser' )
-                       ->will( $this->returnValue( false ) );
-               try {
-                       $provider->preventSessionsForUser( 'Foo' );
-                       $this->fail( 'Expected exception not thrown' );
-               } catch ( \BadMethodCallException $ex ) {
-                       $this->assertSame(
-                               'MediaWiki\\Session\\SessionProvider::preventSessionsForUser must be implemented ' .
-                                       'when canChangeUser() is false',
-                               $ex->getMessage()
-                       );
-               }
-       }
-
-       public function testHashToSessionId() {
-               $config = new \HashConfig( [
-                       'SecretKey' => 'Shhh!',
-               ] );
-
-               $provider = $this->getMockForAbstractClass( SessionProvider::class,
-                       [], 'MockSessionProvider' );
-               $provider->setConfig( $config );
-               $priv = TestingAccessWrapper::newFromObject( $provider );
-
-               $this->assertSame( 'eoq8cb1mg7j30ui5qolafps4hg29k5bb', $priv->hashToSessionId( 'foobar' ) );
-               $this->assertSame( '4do8j7tfld1g8tte9jqp3csfgmulaun9',
-                       $priv->hashToSessionId( 'foobar', 'secret' ) );
-
-               try {
-                       $priv->hashToSessionId( [] );
-                       $this->fail( 'Expected exception not thrown' );
-               } catch ( \InvalidArgumentException $ex ) {
-                       $this->assertSame(
-                               '$data must be a string, array was passed',
-                               $ex->getMessage()
-                       );
-               }
-               try {
-                       $priv->hashToSessionId( '', false );
-                       $this->fail( 'Expected exception not thrown' );
-               } catch ( \InvalidArgumentException $ex ) {
-                       $this->assertSame(
-                               '$key must be a string or null, boolean was passed',
-                               $ex->getMessage()
-                       );
-               }
-       }
-
-       public function testDescribe() {
-               $provider = $this->getMockForAbstractClass( SessionProvider::class,
-                       [], 'MockSessionProvider' );
-
-               $this->assertSame(
-                       'MockSessionProvider sessions',
-                       $provider->describe( \Language::factory( 'en' ) )
-               );
-       }
-
-       public function testGetAllowedUserRights() {
-               $provider = $this->getMockForAbstractClass( SessionProvider::class );
-               $backend = TestUtils::getDummySessionBackend();
-
-               try {
-                       $provider->getAllowedUserRights( $backend );
-                       $this->fail( 'Expected exception not thrown' );
-               } catch ( \InvalidArgumentException $ex ) {
-                       $this->assertSame(
-                               'Backend\'s provider isn\'t $this',
-                               $ex->getMessage()
-                       );
-               }
-
-               TestingAccessWrapper::newFromObject( $backend )->provider = $provider;
-               $this->assertNull( $provider->getAllowedUserRights( $backend ) );
-       }
-
-}
diff --git a/tests/phpunit/includes/session/SessionTest.php b/tests/phpunit/includes/session/SessionTest.php
deleted file mode 100644 (file)
index a74056d..0000000
+++ /dev/null
@@ -1,373 +0,0 @@
-<?php
-
-namespace MediaWiki\Session;
-
-use Psr\Log\LogLevel;
-use MediaWikiTestCase;
-use User;
-use Wikimedia\TestingAccessWrapper;
-
-/**
- * @group Session
- * @covers MediaWiki\Session\Session
- */
-class SessionTest extends MediaWikiTestCase {
-
-       public function testConstructor() {
-               $backend = TestUtils::getDummySessionBackend();
-               TestingAccessWrapper::newFromObject( $backend )->requests = [ -1 => 'dummy' ];
-               TestingAccessWrapper::newFromObject( $backend )->id = new SessionId( 'abc' );
-
-               $session = new Session( $backend, 42, new \TestLogger );
-               $priv = TestingAccessWrapper::newFromObject( $session );
-               $this->assertSame( $backend, $priv->backend );
-               $this->assertSame( 42, $priv->index );
-
-               $request = new \FauxRequest();
-               $priv2 = TestingAccessWrapper::newFromObject( $session->sessionWithRequest( $request ) );
-               $this->assertSame( $backend, $priv2->backend );
-               $this->assertNotSame( $priv->index, $priv2->index );
-               $this->assertSame( $request, $priv2->getRequest() );
-       }
-
-       /**
-        * @dataProvider provideMethods
-        * @param string $m Method to test
-        * @param array $args Arguments to pass to the method
-        * @param bool $index Whether the backend method gets passed the index
-        * @param bool $ret Whether the method returns a value
-        */
-       public function testMethods( $m, $args, $index, $ret ) {
-               $mock = $this->getMockBuilder( DummySessionBackend::class )
-                       ->setMethods( [ $m, 'deregisterSession' ] )
-                       ->getMock();
-               $mock->expects( $this->once() )->method( 'deregisterSession' )
-                       ->with( $this->identicalTo( 42 ) );
-
-               $tmp = $mock->expects( $this->once() )->method( $m );
-               $expectArgs = [];
-               if ( $index ) {
-                       $expectArgs[] = $this->identicalTo( 42 );
-               }
-               foreach ( $args as $arg ) {
-                       $expectArgs[] = $this->identicalTo( $arg );
-               }
-               $tmp = call_user_func_array( [ $tmp, 'with' ], $expectArgs );
-
-               $retval = new \stdClass;
-               $tmp->will( $this->returnValue( $retval ) );
-
-               $session = TestUtils::getDummySession( $mock, 42 );
-
-               if ( $ret ) {
-                       $this->assertSame( $retval, call_user_func_array( [ $session, $m ], $args ) );
-               } else {
-                       $this->assertNull( call_user_func_array( [ $session, $m ], $args ) );
-               }
-
-               // Trigger Session destructor
-               $session = null;
-       }
-
-       public static function provideMethods() {
-               return [
-                       [ 'getId', [], false, true ],
-                       [ 'getSessionId', [], false, true ],
-                       [ 'resetId', [], false, true ],
-                       [ 'getProvider', [], false, true ],
-                       [ 'isPersistent', [], false, true ],
-                       [ 'persist', [], false, false ],
-                       [ 'unpersist', [], false, false ],
-                       [ 'shouldRememberUser', [], false, true ],
-                       [ 'setRememberUser', [ true ], false, false ],
-                       [ 'getRequest', [], true, true ],
-                       [ 'getUser', [], false, true ],
-                       [ 'getAllowedUserRights', [], false, true ],
-                       [ 'canSetUser', [], false, true ],
-                       [ 'setUser', [ new \stdClass ], false, false ],
-                       [ 'suggestLoginUsername', [], true, true ],
-                       [ 'shouldForceHTTPS', [], false, true ],
-                       [ 'setForceHTTPS', [ true ], false, false ],
-                       [ 'getLoggedOutTimestamp', [], false, true ],
-                       [ 'setLoggedOutTimestamp', [ 123 ], false, false ],
-                       [ 'getProviderMetadata', [], false, true ],
-                       [ 'save', [], false, false ],
-                       [ 'delaySave', [], false, true ],
-                       [ 'renew', [], false, false ],
-               ];
-       }
-
-       public function testDataAccess() {
-               $session = TestUtils::getDummySession();
-               $backend = TestingAccessWrapper::newFromObject( $session )->backend;
-
-               $this->assertEquals( 1, $session->get( 'foo' ) );
-               $this->assertEquals( 'zero', $session->get( 0 ) );
-               $this->assertFalse( $backend->dirty );
-
-               $this->assertEquals( null, $session->get( 'null' ) );
-               $this->assertEquals( 'default', $session->get( 'null', 'default' ) );
-               $this->assertFalse( $backend->dirty );
-
-               $session->set( 'foo', 55 );
-               $this->assertEquals( 55, $backend->data['foo'] );
-               $this->assertTrue( $backend->dirty );
-               $backend->dirty = false;
-
-               $session->set( 1, 'one' );
-               $this->assertEquals( 'one', $backend->data[1] );
-               $this->assertTrue( $backend->dirty );
-               $backend->dirty = false;
-
-               $session->set( 1, 'one' );
-               $this->assertFalse( $backend->dirty );
-
-               $this->assertTrue( $session->exists( 'foo' ) );
-               $this->assertTrue( $session->exists( 1 ) );
-               $this->assertFalse( $session->exists( 'null' ) );
-               $this->assertFalse( $session->exists( 100 ) );
-               $this->assertFalse( $backend->dirty );
-
-               $session->remove( 'foo' );
-               $this->assertArrayNotHasKey( 'foo', $backend->data );
-               $this->assertTrue( $backend->dirty );
-               $backend->dirty = false;
-               $session->remove( 1 );
-               $this->assertArrayNotHasKey( 1, $backend->data );
-               $this->assertTrue( $backend->dirty );
-               $backend->dirty = false;
-
-               $session->remove( 101 );
-               $this->assertFalse( $backend->dirty );
-
-               $backend->data = [ 'a', 'b', '?' => 'c' ];
-               $this->assertSame( 3, $session->count() );
-               $this->assertSame( 3, count( $session ) );
-               $this->assertFalse( $backend->dirty );
-
-               $data = [];
-               foreach ( $session as $key => $value ) {
-                       $data[$key] = $value;
-               }
-               $this->assertEquals( $backend->data, $data );
-               $this->assertFalse( $backend->dirty );
-
-               $this->assertEquals( $backend->data, iterator_to_array( $session ) );
-               $this->assertFalse( $backend->dirty );
-       }
-
-       public function testArrayAccess() {
-               $logger = new \TestLogger;
-               $session = TestUtils::getDummySession( null, -1, $logger );
-               $backend = TestingAccessWrapper::newFromObject( $session )->backend;
-
-               $this->assertEquals( 1, $session['foo'] );
-               $this->assertEquals( 'zero', $session[0] );
-               $this->assertFalse( $backend->dirty );
-
-               $logger->setCollect( true );
-               $this->assertEquals( null, $session['null'] );
-               $logger->setCollect( false );
-               $this->assertFalse( $backend->dirty );
-               $this->assertSame( [
-                       [ LogLevel::DEBUG, 'Undefined index (auto-adds to session with a null value): null' ]
-               ], $logger->getBuffer() );
-               $logger->clearBuffer();
-
-               $session['foo'] = 55;
-               $this->assertEquals( 55, $backend->data['foo'] );
-               $this->assertTrue( $backend->dirty );
-               $backend->dirty = false;
-
-               $session[1] = 'one';
-               $this->assertEquals( 'one', $backend->data[1] );
-               $this->assertTrue( $backend->dirty );
-               $backend->dirty = false;
-
-               $session[1] = 'one';
-               $this->assertFalse( $backend->dirty );
-
-               $session['bar'] = [ 'baz' => [] ];
-               $session['bar']['baz']['quux'] = 2;
-               $this->assertEquals( [ 'baz' => [ 'quux' => 2 ] ], $backend->data['bar'] );
-
-               $logger->setCollect( true );
-               $session['bar2']['baz']['quux'] = 3;
-               $logger->setCollect( false );
-               $this->assertEquals( [ 'baz' => [ 'quux' => 3 ] ], $backend->data['bar2'] );
-               $this->assertSame( [
-                       [ LogLevel::DEBUG, 'Undefined index (auto-adds to session with a null value): bar2' ]
-               ], $logger->getBuffer() );
-               $logger->clearBuffer();
-
-               $backend->dirty = false;
-               $this->assertTrue( isset( $session['foo'] ) );
-               $this->assertTrue( isset( $session[1] ) );
-               $this->assertFalse( isset( $session['null'] ) );
-               $this->assertFalse( isset( $session['missing'] ) );
-               $this->assertFalse( isset( $session[100] ) );
-               $this->assertFalse( $backend->dirty );
-
-               unset( $session['foo'] );
-               $this->assertArrayNotHasKey( 'foo', $backend->data );
-               $this->assertTrue( $backend->dirty );
-               $backend->dirty = false;
-               unset( $session[1] );
-               $this->assertArrayNotHasKey( 1, $backend->data );
-               $this->assertTrue( $backend->dirty );
-               $backend->dirty = false;
-
-               unset( $session[101] );
-               $this->assertFalse( $backend->dirty );
-       }
-
-       public function testClear() {
-               $session = TestUtils::getDummySession();
-               $priv = TestingAccessWrapper::newFromObject( $session );
-
-               $backend = $this->getMockBuilder( DummySessionBackend::class )
-                       ->setMethods( [ 'canSetUser', 'setUser', 'save' ] )
-                       ->getMock();
-               $backend->expects( $this->once() )->method( 'canSetUser' )
-                       ->will( $this->returnValue( true ) );
-               $backend->expects( $this->once() )->method( 'setUser' )
-                       ->with( $this->callback( function ( $user ) {
-                               return $user instanceof User && $user->isAnon();
-                       } ) );
-               $backend->expects( $this->once() )->method( 'save' );
-               $priv->backend = $backend;
-               $session->clear();
-               $this->assertSame( [], $backend->data );
-               $this->assertTrue( $backend->dirty );
-
-               $backend = $this->getMockBuilder( DummySessionBackend::class )
-                       ->setMethods( [ 'canSetUser', 'setUser', 'save' ] )
-                       ->getMock();
-               $backend->data = [];
-               $backend->expects( $this->once() )->method( 'canSetUser' )
-                       ->will( $this->returnValue( true ) );
-               $backend->expects( $this->once() )->method( 'setUser' )
-                       ->with( $this->callback( function ( $user ) {
-                               return $user instanceof User && $user->isAnon();
-                       } ) );
-               $backend->expects( $this->once() )->method( 'save' );
-               $priv->backend = $backend;
-               $session->clear();
-               $this->assertFalse( $backend->dirty );
-
-               $backend = $this->getMockBuilder( DummySessionBackend::class )
-                       ->setMethods( [ 'canSetUser', 'setUser', 'save' ] )
-                       ->getMock();
-               $backend->expects( $this->once() )->method( 'canSetUser' )
-                       ->will( $this->returnValue( false ) );
-               $backend->expects( $this->never() )->method( 'setUser' );
-               $backend->expects( $this->once() )->method( 'save' );
-               $priv->backend = $backend;
-               $session->clear();
-               $this->assertSame( [], $backend->data );
-               $this->assertTrue( $backend->dirty );
-       }
-
-       public function testTokens() {
-               $session = TestUtils::getDummySession();
-               $priv = TestingAccessWrapper::newFromObject( $session );
-               $backend = $priv->backend;
-
-               $token = TestingAccessWrapper::newFromObject( $session->getToken() );
-               $this->assertArrayHasKey( 'wsTokenSecrets', $backend->data );
-               $this->assertArrayHasKey( 'default', $backend->data['wsTokenSecrets'] );
-               $secret = $backend->data['wsTokenSecrets']['default'];
-               $this->assertSame( $secret, $token->secret );
-               $this->assertSame( '', $token->salt );
-               $this->assertTrue( $token->wasNew() );
-
-               $token = TestingAccessWrapper::newFromObject( $session->getToken( 'foo' ) );
-               $this->assertSame( $secret, $token->secret );
-               $this->assertSame( 'foo', $token->salt );
-               $this->assertFalse( $token->wasNew() );
-
-               $backend->data['wsTokenSecrets']['secret'] = 'sekret';
-               $token = TestingAccessWrapper::newFromObject(
-                       $session->getToken( [ 'bar', 'baz' ], 'secret' )
-               );
-               $this->assertSame( 'sekret', $token->secret );
-               $this->assertSame( 'bar|baz', $token->salt );
-               $this->assertFalse( $token->wasNew() );
-
-               $session->resetToken( 'secret' );
-               $this->assertArrayHasKey( 'wsTokenSecrets', $backend->data );
-               $this->assertArrayHasKey( 'default', $backend->data['wsTokenSecrets'] );
-               $this->assertArrayNotHasKey( 'secret', $backend->data['wsTokenSecrets'] );
-
-               $session->resetAllTokens();
-               $this->assertArrayNotHasKey( 'wsTokenSecrets', $backend->data );
-       }
-
-       /**
-        * @dataProvider provideSecretsRoundTripping
-        * @param mixed $data
-        */
-       public function testSecretsRoundTripping( $data ) {
-               $session = TestUtils::getDummySession();
-
-               // Simple round-trip
-               $session->setSecret( 'secret', $data );
-               $this->assertNotEquals( $data, $session->get( 'secret' ) );
-               $this->assertEquals( $data, $session->getSecret( 'secret', 'defaulted' ) );
-       }
-
-       public static function provideSecretsRoundTripping() {
-               return [
-                       [ 'Foobar' ],
-                       [ 42 ],
-                       [ [ 'foo', 'bar' => 'baz', 'subarray' => [ 1, 2, 3 ] ] ],
-                       [ (object)[ 'foo', 'bar' => 'baz', 'subarray' => [ 1, 2, 3 ] ] ],
-                       [ true ],
-                       [ false ],
-                       [ null ],
-               ];
-       }
-
-       public function testSecrets() {
-               $logger = new \TestLogger;
-               $session = TestUtils::getDummySession( null, -1, $logger );
-
-               // Simple defaulting
-               $this->assertEquals( 'defaulted', $session->getSecret( 'test', 'defaulted' ) );
-
-               // Bad encrypted data
-               $session->set( 'test', 'foobar' );
-               $logger->setCollect( true );
-               $this->assertEquals( 'defaulted', $session->getSecret( 'test', 'defaulted' ) );
-               $logger->setCollect( false );
-               $this->assertSame( [
-                       [ LogLevel::WARNING, 'Invalid sealed-secret format' ]
-               ], $logger->getBuffer() );
-               $logger->clearBuffer();
-
-               // Tampered data
-               $session->setSecret( 'test', 'foobar' );
-               $encrypted = $session->get( 'test' );
-               $session->set( 'test', $encrypted . 'x' );
-               $logger->setCollect( true );
-               $this->assertEquals( 'defaulted', $session->getSecret( 'test', 'defaulted' ) );
-               $logger->setCollect( false );
-               $this->assertSame( [
-                       [ LogLevel::WARNING, 'Sealed secret has been tampered with, aborting.' ]
-               ], $logger->getBuffer() );
-               $logger->clearBuffer();
-
-               // Unserializable data
-               $iv = random_bytes( 16 );
-               list( $encKey, $hmacKey ) = TestingAccessWrapper::newFromObject( $session )->getSecretKeys();
-               $ciphertext = openssl_encrypt( 'foobar', 'aes-256-ctr', $encKey, OPENSSL_RAW_DATA, $iv );
-               $sealed = base64_encode( $iv ) . '.' . base64_encode( $ciphertext );
-               $hmac = hash_hmac( 'sha256', $sealed, $hmacKey, true );
-               $encrypted = base64_encode( $hmac ) . '.' . $sealed;
-               $session->set( 'test', $encrypted );
-               \Wikimedia\suppressWarnings();
-               $this->assertEquals( 'defaulted', $session->getSecret( 'test', 'defaulted' ) );
-               \Wikimedia\restoreWarnings();
-       }
-
-}
diff --git a/tests/phpunit/includes/session/TokenTest.php b/tests/phpunit/includes/session/TokenTest.php
deleted file mode 100644 (file)
index 4797652..0000000
+++ /dev/null
@@ -1,67 +0,0 @@
-<?php
-
-namespace MediaWiki\Session;
-
-use MediaWikiTestCase;
-use Wikimedia\TestingAccessWrapper;
-
-/**
- * @group Session
- * @covers MediaWiki\Session\Token
- */
-class TokenTest extends MediaWikiTestCase {
-
-       public function testBasics() {
-               $token = $this->getMockBuilder( Token::class )
-                       ->setMethods( [ 'toStringAtTimestamp' ] )
-                       ->setConstructorArgs( [ 'sekret', 'salty', true ] )
-                       ->getMock();
-               $token->expects( $this->any() )->method( 'toStringAtTimestamp' )
-                       ->will( $this->returnValue( 'faketoken+\\' ) );
-
-               $this->assertSame( 'faketoken+\\', $token->toString() );
-               $this->assertSame( 'faketoken+\\', (string)$token );
-               $this->assertTrue( $token->wasNew() );
-
-               $token = new Token( 'sekret', 'salty', false );
-               $this->assertFalse( $token->wasNew() );
-       }
-
-       public function testToStringAtTimestamp() {
-               $token = TestingAccessWrapper::newFromObject( new Token( 'sekret', 'salty', false ) );
-
-               $this->assertSame(
-                       'd9ade0c7d4349e9df9094e61c33a5a0d5644fde2+\\',
-                       $token->toStringAtTimestamp( 1447362018 )
-               );
-               $this->assertSame(
-                       'ee2f7a2488dea9176c224cfb400d43be5644fdea+\\',
-                       $token->toStringAtTimestamp( 1447362026 )
-               );
-       }
-
-       public function testGetTimestamp() {
-               $this->assertSame(
-                       1447362018, Token::getTimestamp( 'd9ade0c7d4349e9df9094e61c33a5a0d5644fde2+\\' )
-               );
-               $this->assertSame(
-                       1447362026, Token::getTimestamp( 'ee2f7a2488dea9176c224cfb400d43be5644fdea+\\' )
-               );
-               $this->assertNull( Token::getTimestamp( 'ee2f7a2488dea9176c224cfb400d43be5644fdea-\\' ) );
-               $this->assertNull( Token::getTimestamp( 'ee2f7a2488dea9176c224cfb400d43be+\\' ) );
-
-               $this->assertNull( Token::getTimestamp( 'ee2f7a2488dea9x76c224cfb400d43be5644fdea+\\' ) );
-       }
-
-       public function testMatch() {
-               $token = TestingAccessWrapper::newFromObject( new Token( 'sekret', 'salty', false ) );
-
-               $test = $token->toStringAtTimestamp( time() - 10 );
-               $this->assertTrue( $token->match( $test ) );
-               $this->assertTrue( $token->match( $test, 12 ) );
-               $this->assertFalse( $token->match( $test, 8 ) );
-
-               $this->assertFalse( $token->match( 'ee2f7a2488dea9176c224cfb400d43be5644fdea-\\' ) );
-       }
-
-}
diff --git a/tests/phpunit/includes/shell/CommandFactoryTest.php b/tests/phpunit/includes/shell/CommandFactoryTest.php
deleted file mode 100644 (file)
index b031431..0000000
+++ /dev/null
@@ -1,50 +0,0 @@
-<?php
-
-use MediaWiki\Shell\Command;
-use MediaWiki\Shell\CommandFactory;
-use MediaWiki\Shell\FirejailCommand;
-use Psr\Log\NullLogger;
-use Wikimedia\TestingAccessWrapper;
-
-/**
- * @group Shell
- */
-class CommandFactoryTest extends PHPUnit\Framework\TestCase {
-
-       use MediaWikiCoversValidator;
-
-       /**
-        * @covers MediaWiki\Shell\CommandFactory::create
-        */
-       public function testCreate() {
-               $logger = new NullLogger();
-               $cgroup = '/sys/fs/cgroup/memory/mygroup';
-               $limits = [
-                       'filesize' => 1000,
-                       'memory' => 1000,
-                       'time' => 30,
-                       'walltime' => 40,
-               ];
-
-               $factory = new CommandFactory( $limits, $cgroup, false );
-               $factory->setLogger( $logger );
-               $factory->logStderr();
-               $command = $factory->create();
-               $this->assertInstanceOf( Command::class, $command );
-
-               $wrapper = TestingAccessWrapper::newFromObject( $command );
-               $this->assertSame( $logger, $wrapper->logger );
-               $this->assertSame( $cgroup, $wrapper->cgroup );
-               $this->assertSame( $limits, $wrapper->limits );
-               $this->assertTrue( $wrapper->doLogStderr );
-       }
-
-       /**
-        * @covers MediaWiki\Shell\CommandFactory::create
-        */
-       public function testFirejailCreate() {
-               $factory = new CommandFactory( [], false, 'firejail' );
-               $factory->setLogger( new NullLogger() );
-               $this->assertInstanceOf( FirejailCommand::class, $factory->create() );
-       }
-}
diff --git a/tests/phpunit/includes/shell/CommandTest.php b/tests/phpunit/includes/shell/CommandTest.php
deleted file mode 100644 (file)
index 2e03163..0000000
+++ /dev/null
@@ -1,181 +0,0 @@
-<?php
-
-use MediaWiki\Shell\Command;
-use Wikimedia\TestingAccessWrapper;
-
-/**
- * @covers \MediaWiki\Shell\Command
- * @group Shell
- */
-class CommandTest extends PHPUnit\Framework\TestCase {
-
-       use MediaWikiCoversValidator;
-
-       private function requirePosix() {
-               if ( wfIsWindows() ) {
-                       $this->markTestSkipped( 'This test requires a POSIX environment.' );
-               }
-       }
-
-       /**
-        * @dataProvider provideExecute
-        */
-       public function testExecute( $commandInput, $expectedExitCode, $expectedOutput ) {
-               $this->requirePosix();
-
-               $command = new Command();
-               $result = $command
-                       ->params( $commandInput )
-                       ->execute();
-
-               $this->assertSame( $expectedExitCode, $result->getExitCode() );
-               $this->assertSame( $expectedOutput, $result->getStdout() );
-       }
-
-       public function provideExecute() {
-               return [
-                       'success status' => [ 'true', 0, '' ],
-                       'failure status' => [ 'false', 1, '' ],
-                       'output' => [ [ 'echo', '-n', 'x', '>', 'y' ], 0, 'x > y' ],
-               ];
-       }
-
-       public function testEnvironment() {
-               $this->requirePosix();
-
-               $command = new Command();
-               $result = $command
-                       ->params( [ 'printenv', 'FOO' ] )
-                       ->environment( [ 'FOO' => 'bar' ] )
-                       ->execute();
-               $this->assertSame( "bar\n", $result->getStdout() );
-       }
-
-       public function testStdout() {
-               $this->requirePosix();
-
-               $command = new Command();
-
-               $result = $command
-                       ->params( 'bash', '-c', 'echo ThisIsStderr 1>&2' )
-                       ->execute();
-
-               $this->assertNotContains( 'ThisIsStderr', $result->getStdout() );
-               $this->assertEquals( "ThisIsStderr\n", $result->getStderr() );
-       }
-
-       public function testStdoutRedirection() {
-               $this->requirePosix();
-
-               $command = new Command();
-
-               $result = $command
-                       ->params( 'bash', '-c', 'echo ThisIsStderr 1>&2' )
-                       ->includeStderr( true )
-                       ->execute();
-
-               $this->assertEquals( "ThisIsStderr\n", $result->getStdout() );
-               $this->assertNull( $result->getStderr() );
-       }
-
-       public function testOutput() {
-               global $IP;
-
-               $this->requirePosix();
-               chdir( $IP );
-
-               $command = new Command();
-               $result = $command
-                       ->params( [ 'ls', 'index.php' ] )
-                       ->execute();
-               $this->assertRegExp( '/^index.php$/m', $result->getStdout() );
-               $this->assertSame( null, $result->getStderr() );
-
-               $command = new Command();
-               $result = $command
-                       ->params( [ 'ls', 'index.php', 'no-such-file' ] )
-                       ->includeStderr()
-                       ->execute();
-               $this->assertRegExp( '/^index.php$/m', $result->getStdout() );
-               $this->assertRegExp( '/^.+no-such-file.*$/m', $result->getStdout() );
-               $this->assertSame( null, $result->getStderr() );
-
-               $command = new Command();
-               $result = $command
-                       ->params( [ 'ls', 'index.php', 'no-such-file' ] )
-                       ->execute();
-               $this->assertRegExp( '/^index.php$/m', $result->getStdout() );
-               $this->assertRegExp( '/^.+no-such-file.*$/m', $result->getStderr() );
-       }
-
-       /**
-        * Test that null values are skipped by params() and unsafeParams()
-        */
-       public function testNullsAreSkipped() {
-               $command = TestingAccessWrapper::newFromObject( new Command );
-               $command->params( 'echo', 'a', null, 'b' );
-               $command->unsafeParams( 'c', null, 'd' );
-               $this->assertEquals( "'echo' 'a' 'b' c d", $command->command );
-       }
-
-       public function testT69870() {
-               $commandLine = wfIsWindows()
-                       // 333 = 331 + CRLF
-                       ? ( 'for /l %i in (1, 1, 1001) do @echo ' . str_repeat( '*', 331 ) )
-                       : 'printf "%-333333s" "*"';
-
-               // Test several times because it involves a race condition that may randomly succeed or fail
-               for ( $i = 0; $i < 10; $i++ ) {
-                       $command = new Command();
-                       $output = $command->unsafeParams( $commandLine )
-                               ->execute()
-                               ->getStdout();
-                       $this->assertEquals( 333333, strlen( $output ) );
-               }
-       }
-
-       public function testLogStderr() {
-               $this->requirePosix();
-
-               $logger = new TestLogger( true, function ( $message, $level, $context ) {
-                       return $level === Psr\Log\LogLevel::ERROR ? '1' : null;
-               }, true );
-               $command = new Command();
-               $command->setLogger( $logger );
-               $command->params( 'bash', '-c', 'echo ThisIsStderr 1>&2' );
-               $command->execute();
-               $this->assertEmpty( $logger->getBuffer() );
-
-               $command = new Command();
-               $command->setLogger( $logger );
-               $command->logStderr();
-               $command->params( 'bash', '-c', 'echo ThisIsStderr 1>&2' );
-               $command->execute();
-               $this->assertSame( 1, count( $logger->getBuffer() ) );
-               $this->assertSame( trim( $logger->getBuffer()[0][2]['error'] ), 'ThisIsStderr' );
-       }
-
-       public function testInput() {
-               $this->requirePosix();
-
-               $command = new Command();
-               $command->params( 'cat' );
-               $command->input( 'abc' );
-               $result = $command->execute();
-               $this->assertSame( 'abc', $result->getStdout() );
-
-               // now try it with something that does not fit into a single block
-               $command = new Command();
-               $command->params( 'cat' );
-               $command->input( str_repeat( '!', 1000000 ) );
-               $result = $command->execute();
-               $this->assertSame( 1000000, strlen( $result->getStdout() ) );
-
-               // And try it with empty input
-               $command = new Command();
-               $command->params( 'cat' );
-               $command->input( '' );
-               $result = $command->execute();
-               $this->assertSame( '', $result->getStdout() );
-       }
-}
diff --git a/tests/phpunit/includes/shell/FirejailCommandTest.php b/tests/phpunit/includes/shell/FirejailCommandTest.php
deleted file mode 100644 (file)
index 681c3dc..0000000
+++ /dev/null
@@ -1,85 +0,0 @@
-<?php
-
-/**
- * Copyright (C) 2017 Kunal Mehta <legoktm@member.fsf.org>
- *
- * 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; either version 2 of the License, or
- * (at your option) any later version.
- *
- * 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.
- *
- */
-
-use MediaWiki\Shell\FirejailCommand;
-use MediaWiki\Shell\Shell;
-use Wikimedia\TestingAccessWrapper;
-
-class FirejailCommandTest extends PHPUnit\Framework\TestCase {
-
-       use MediaWikiCoversValidator;
-
-       public function provideBuildFinalCommand() {
-               global $IP;
-               // phpcs:ignore Generic.Files.LineLength
-               $env = "'MW_INCLUDE_STDERR=;MW_CPU_LIMIT=180; MW_CGROUP='\'''\''; MW_MEM_LIMIT=307200; MW_FILE_SIZE_LIMIT=102400; MW_WALL_CLOCK_LIMIT=180; MW_USE_LOG_PIPE=yes'";
-               $limit = "/bin/bash '$IP/includes/shell/limit.sh'";
-               $profile = "--profile=$IP/includes/shell/firejail.profile";
-               $blacklist = '--blacklist=' . realpath( MW_CONFIG_FILE );
-               $default = "$blacklist --noroot --seccomp --private-dev";
-               return [
-                       [
-                               'No restrictions',
-                               'ls', 0, "$limit ''\''ls'\''' $env"
-                       ],
-                       [
-                               'default restriction',
-                               'ls', Shell::RESTRICT_DEFAULT,
-                               "$limit 'firejail --quiet $profile $default -- '\''ls'\''' $env"
-                       ],
-                       [
-                               'no network',
-                               'ls', Shell::NO_NETWORK,
-                               "$limit 'firejail --quiet $profile --net=none -- '\''ls'\''' $env"
-                       ],
-                       [
-                               'default restriction & no network',
-                               'ls', Shell::RESTRICT_DEFAULT | Shell::NO_NETWORK,
-                               "$limit 'firejail --quiet $profile $default --net=none -- '\''ls'\''' $env"
-                       ],
-                       [
-                               'seccomp',
-                               'ls', Shell::SECCOMP,
-                               "$limit 'firejail --quiet $profile --seccomp -- '\''ls'\''' $env"
-                       ],
-                       [
-                               'seccomp & no execve',
-                               'ls', Shell::SECCOMP | Shell::NO_EXECVE,
-                               "$limit 'firejail --quiet $profile --shell=none --seccomp=execve -- '\''ls'\''' $env"
-                       ],
-               ];
-       }
-
-       /**
-        * @covers \MediaWiki\Shell\FirejailCommand::buildFinalCommand()
-        * @dataProvider provideBuildFinalCommand
-        */
-       public function testBuildFinalCommand( $desc, $params, $flags, $expected ) {
-               $command = new FirejailCommand( 'firejail' );
-               $command
-                       ->params( $params )
-                       ->restrict( $flags );
-               $wrapper = TestingAccessWrapper::newFromObject( $command );
-               $output = $wrapper->buildFinalCommand( $wrapper->command );
-               $this->assertEquals( $expected, $output[0], $desc );
-       }
-
-}
diff --git a/tests/phpunit/includes/site/CachingSiteStoreTest.php b/tests/phpunit/includes/site/CachingSiteStoreTest.php
deleted file mode 100644 (file)
index f04d35c..0000000
+++ /dev/null
@@ -1,167 +0,0 @@
-<?php
-
-/**
- * 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; either version 2 of the License, or
- * (at your option) any later version.
- *
- * 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.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- * @since 1.25
- *
- * @ingroup Site
- * @ingroup Test
- *
- * @group Site
- * @group Database
- *
- * @author Jeroen De Dauw < jeroendedauw@gmail.com >
- */
-class CachingSiteStoreTest extends MediaWikiTestCase {
-
-       /**
-        * @covers CachingSiteStore::getSites
-        */
-       public function testGetSites() {
-               $testSites = TestSites::getSites();
-
-               $store = new CachingSiteStore(
-                       $this->getHashSiteStore( $testSites ),
-                       ObjectCache::getLocalClusterInstance()
-               );
-
-               $sites = $store->getSites();
-
-               $this->assertInstanceOf( SiteList::class, $sites );
-
-               /**
-                * @var Site $site
-                */
-               foreach ( $sites as $site ) {
-                       $this->assertInstanceOf( Site::class, $site );
-               }
-
-               foreach ( $testSites as $site ) {
-                       if ( $site->getGlobalId() !== null ) {
-                               $this->assertTrue( $sites->hasSite( $site->getGlobalId() ) );
-                       }
-               }
-       }
-
-       /**
-        * @covers CachingSiteStore::saveSites
-        */
-       public function testSaveSites() {
-               $store = new CachingSiteStore(
-                       new HashSiteStore(), ObjectCache::getLocalClusterInstance()
-               );
-
-               $sites = [];
-
-               $site = new Site();
-               $site->setGlobalId( 'ertrywuutr' );
-               $site->setLanguageCode( 'en' );
-               $sites[] = $site;
-
-               $site = new MediaWikiSite();
-               $site->setGlobalId( 'sdfhxujgkfpth' );
-               $site->setLanguageCode( 'nl' );
-               $sites[] = $site;
-
-               $this->assertTrue( $store->saveSites( $sites ) );
-
-               $site = $store->getSite( 'ertrywuutr' );
-               $this->assertInstanceOf( Site::class, $site );
-               $this->assertEquals( 'en', $site->getLanguageCode() );
-
-               $site = $store->getSite( 'sdfhxujgkfpth' );
-               $this->assertInstanceOf( Site::class, $site );
-               $this->assertEquals( 'nl', $site->getLanguageCode() );
-       }
-
-       /**
-        * @covers CachingSiteStore::reset
-        */
-       public function testReset() {
-               $dbSiteStore = $this->getMockBuilder( SiteStore::class )
-                       ->disableOriginalConstructor()
-                       ->getMock();
-
-               $dbSiteStore->expects( $this->any() )
-                       ->method( 'getSite' )
-                       ->will( $this->returnValue( $this->getTestSite() ) );
-
-               $dbSiteStore->expects( $this->any() )
-                       ->method( 'getSites' )
-                       ->will( $this->returnCallback( function () {
-                               $siteList = new SiteList();
-                               $siteList->setSite( $this->getTestSite() );
-
-                               return $siteList;
-                       } ) );
-
-               $store = new CachingSiteStore( $dbSiteStore, ObjectCache::getLocalClusterInstance() );
-
-               // initialize internal cache
-               $this->assertGreaterThan( 0, $store->getSites()->count(), 'count sites' );
-
-               $store->getSite( 'enwiki' )->setLanguageCode( 'en-ca' );
-
-               // sanity check: $store should have the new language code for 'enwiki'
-               $this->assertEquals( 'en-ca', $store->getSite( 'enwiki' )->getLanguageCode(), 'sanity check' );
-
-               // purge cache
-               $store->reset();
-
-               // the internal cache of $store should be updated, and now pulling
-               // the site from the 'fallback' DBSiteStore with the original language code.
-               $this->assertEquals( 'en', $store->getSite( 'enwiki' )->getLanguageCode(), 'reset' );
-       }
-
-       public function getTestSite() {
-               $enwiki = new MediaWikiSite();
-               $enwiki->setGlobalId( 'enwiki' );
-               $enwiki->setLanguageCode( 'en' );
-
-               return $enwiki;
-       }
-
-       /**
-        * @covers CachingSiteStore::clear
-        */
-       public function testClear() {
-               $store = new CachingSiteStore(
-                       new HashSiteStore(), ObjectCache::getLocalClusterInstance()
-               );
-               $this->assertTrue( $store->clear() );
-
-               $site = $store->getSite( 'enwiki' );
-               $this->assertNull( $site );
-
-               $sites = $store->getSites();
-               $this->assertEquals( 0, $sites->count() );
-       }
-
-       /**
-        * @param Site[] $sites
-        *
-        * @return SiteStore
-        */
-       private function getHashSiteStore( array $sites ) {
-               $siteStore = new HashSiteStore();
-               $siteStore->saveSites( $sites );
-
-               return $siteStore;
-       }
-
-}
diff --git a/tests/phpunit/includes/site/HashSiteStoreTest.php b/tests/phpunit/includes/site/HashSiteStoreTest.php
deleted file mode 100644 (file)
index 6269fd3..0000000
+++ /dev/null
@@ -1,105 +0,0 @@
-<?php
-
-/**
- * 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; either version 2 of the License, or
- * (at your option) any later version.
- *
- * 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.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- * @since 1.25
- *
- * @ingroup Site
- * @group Site
- *
- * @author Katie Filbert < aude.wiki@gmail.com >
- */
-class HashSiteStoreTest extends MediaWikiTestCase {
-
-       /**
-        * @covers HashSiteStore::getSites
-        */
-       public function testGetSites() {
-               $expectedSites = [];
-
-               foreach ( TestSites::getSites() as $testSite ) {
-                       $siteId = $testSite->getGlobalId();
-                       $expectedSites[$siteId] = $testSite;
-               }
-
-               $siteStore = new HashSiteStore( $expectedSites );
-
-               $this->assertEquals( new SiteList( $expectedSites ), $siteStore->getSites() );
-       }
-
-       /**
-        * @covers HashSiteStore::saveSite
-        * @covers HashSiteStore::getSite
-        */
-       public function testSaveSite() {
-               $store = new HashSiteStore();
-
-               $site = new Site();
-               $site->setGlobalId( 'dewiki' );
-
-               $this->assertCount( 0, $store->getSites(), '0 sites in store' );
-
-               $store->saveSite( $site );
-
-               $this->assertCount( 1, $store->getSites(), 'Store has 1 sites' );
-               $this->assertEquals( $site, $store->getSite( 'dewiki' ), 'Store has dewiki' );
-       }
-
-       /**
-        * @covers HashSiteStore::saveSites
-        */
-       public function testSaveSites() {
-               $store = new HashSiteStore();
-
-               $sites = [];
-
-               $site = new Site();
-               $site->setGlobalId( 'enwiki' );
-               $site->setLanguageCode( 'en' );
-               $sites[] = $site;
-
-               $site = new MediaWikiSite();
-               $site->setGlobalId( 'eswiki' );
-               $site->setLanguageCode( 'es' );
-               $sites[] = $site;
-
-               $this->assertCount( 0, $store->getSites(), '0 sites in store' );
-
-               $store->saveSites( $sites );
-
-               $this->assertCount( 2, $store->getSites(), 'Store has 2 sites' );
-               $this->assertTrue( $store->getSites()->hasSite( 'enwiki' ), 'Store has enwiki' );
-               $this->assertTrue( $store->getSites()->hasSite( 'eswiki' ), 'Store has eswiki' );
-       }
-
-       /**
-        * @covers HashSiteStore::clear
-        */
-       public function testClear() {
-               $store = new HashSiteStore();
-
-               $site = new Site();
-               $site->setGlobalId( 'arwiki' );
-               $store->saveSite( $site );
-
-               $this->assertCount( 1, $store->getSites(), '1 site in store' );
-
-               $store->clear();
-               $this->assertCount( 0, $store->getSites(), '0 sites in store' );
-       }
-}
diff --git a/tests/phpunit/includes/site/MediaWikiPageNameNormalizerTest.php b/tests/phpunit/includes/site/MediaWikiPageNameNormalizerTest.php
deleted file mode 100644 (file)
index 15894a3..0000000
+++ /dev/null
@@ -1,116 +0,0 @@
-<?php
-
-use MediaWiki\Site\MediaWikiPageNameNormalizer;
-
-/**
- * @covers MediaWiki\Site\MediaWikiPageNameNormalizer
- *
- * 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; either version 2 of the License, or
- * (at your option) any later version.
- *
- * 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.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @since 1.27
- *
- * @group Site
- * @group medium
- *
- * @author Marius Hoch
- */
-class MediaWikiPageNameNormalizerTest extends PHPUnit\Framework\TestCase {
-
-       use MediaWikiCoversValidator;
-
-       /**
-        * @dataProvider normalizePageTitleProvider
-        */
-       public function testNormalizePageTitle( $expected, $pageName, $getResponse ) {
-               MediaWikiPageNameNormalizerTestMockHttp::$response = $getResponse;
-
-               $normalizer = new MediaWikiPageNameNormalizer(
-                       new MediaWikiPageNameNormalizerTestMockHttp()
-               );
-
-               $this->assertSame(
-                       $expected,
-                       $normalizer->normalizePageName( $pageName, 'https://www.wikidata.org/w/api.php' )
-               );
-       }
-
-       public function normalizePageTitleProvider() {
-               // Response are taken from wikidata and kkwiki using the following API request
-               // api.php?action=query&prop=info&redirects=1&converttitles=1&format=json&titles=…
-               return [
-                       'universe (Q1)' => [
-                               'Q1',
-                               'Q1',
-                               '{"batchcomplete":"","query":{"pages":{"129":{"pageid":129,"ns":0,'
-                               . '"title":"Q1","contentmodel":"wikibase-item","pagelanguage":"en",'
-                               . '"pagelanguagehtmlcode":"en","pagelanguagedir":"ltr",'
-                               . '"touched":"2016-06-23T05:11:21Z","lastrevid":350004448,"length":58001}}}}'
-                       ],
-                       'Q404 redirects to Q395' => [
-                               'Q395',
-                               'Q404',
-                               '{"batchcomplete":"","query":{"redirects":[{"from":"Q404","to":"Q395"}],"pages"'
-                               . ':{"601":{"pageid":601,"ns":0,"title":"Q395","contentmodel":"wikibase-item",'
-                               . '"pagelanguage":"en","pagelanguagehtmlcode":"en","pagelanguagedir":"ltr",'
-                               . '"touched":"2016-06-23T08:00:20Z","lastrevid":350021914,"length":60108}}}}'
-                       ],
-                       'D converted to Д (Latin to Cyrillic) (taken from kkwiki)' => [
-                               'Д',
-                               'D',
-                               '{"batchcomplete":"","query":{"converted":[{"from":"D","to":"\u0414"}],'
-                               . '"pages":{"510541":{"pageid":510541,"ns":0,"title":"\u0414",'
-                               . '"contentmodel":"wikitext","pagelanguage":"kk","pagelanguagehtmlcode":"kk",'
-                               . '"pagelanguagedir":"ltr","touched":"2015-11-22T09:16:18Z",'
-                               . '"lastrevid":2373618,"length":3501}}}}'
-                       ],
-                       'there is no Q0' => [
-                               false,
-                               'Q0',
-                               '{"batchcomplete":"","query":{"pages":{"-1":{"ns":0,"title":"Q0",'
-                               . '"missing":"","contentmodel":"wikibase-item","pagelanguage":"en",'
-                               . '"pagelanguagehtmlcode":"en","pagelanguagedir":"ltr"}}}}'
-                       ],
-                       'invalid title' => [
-                               false,
-                               '{{',
-                               '{"batchcomplete":"","query":{"pages":{"-1":{"title":"{{",'
-                               . '"invalidreason":"The requested page title contains invalid '
-                               . 'characters: \"{\".","invalid":""}}}}'
-                       ],
-                       'error on get' => [ false, 'ABC', false ]
-               ];
-       }
-
-}
-
-/**
- * @private
- * @see Http
- */
-class MediaWikiPageNameNormalizerTestMockHttp extends Http {
-
-       /**
-        * @var mixed
-        */
-       public static $response;
-
-       public static function get( $url, array $options = [], $caller = __METHOD__ ) {
-               PHPUnit_Framework_Assert::assertInternalType( 'string', $url );
-               PHPUnit_Framework_Assert::assertInternalType( 'string', $caller );
-
-               return self::$response;
-       }
-}
diff --git a/tests/phpunit/includes/site/SiteExporterTest.php b/tests/phpunit/includes/site/SiteExporterTest.php
deleted file mode 100644 (file)
index 97a43f8..0000000
+++ /dev/null
@@ -1,148 +0,0 @@
-<?php
-
-/**
- * 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; either version 2 of the License, or
- * (at your option) any later version.
- *
- * 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.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- *
- * @ingroup Site
- * @ingroup Test
- *
- * @group Site
- *
- * @covers SiteExporter
- *
- * @author Daniel Kinzler
- */
-class SiteExporterTest extends PHPUnit\Framework\TestCase {
-
-       use MediaWikiCoversValidator;
-       use PHPUnit4And6Compat;
-
-       public function testConstructor_InvalidArgument() {
-               $this->setExpectedException( InvalidArgumentException::class );
-
-               new SiteExporter( 'Foo' );
-       }
-
-       public function testExportSites() {
-               $foo = Site::newForType( Site::TYPE_UNKNOWN );
-               $foo->setGlobalId( 'Foo' );
-
-               $acme = Site::newForType( Site::TYPE_UNKNOWN );
-               $acme->setGlobalId( 'acme.com' );
-               $acme->setGroup( 'Test' );
-               $acme->addLocalId( Site::ID_INTERWIKI, 'acme' );
-               $acme->setPath( Site::PATH_LINK, 'http://acme.com/' );
-
-               $tmp = tmpfile();
-               $exporter = new SiteExporter( $tmp );
-
-               $exporter->exportSites( [ $foo, $acme ] );
-
-               fseek( $tmp, 0 );
-               $xml = fread( $tmp, 16 * 1024 );
-
-               $this->assertContains( '<sites ', $xml );
-               $this->assertContains( '<site>', $xml );
-               $this->assertContains( '<globalid>Foo</globalid>', $xml );
-               $this->assertContains( '</site>', $xml );
-               $this->assertContains( '<globalid>acme.com</globalid>', $xml );
-               $this->assertContains( '<group>Test</group>', $xml );
-               $this->assertContains( '<localid type="interwiki">acme</localid>', $xml );
-               $this->assertContains( '<path type="link">http://acme.com/</path>', $xml );
-               $this->assertContains( '</sites>', $xml );
-
-               // NOTE: HHVM (at least on wmf Jenkins) doesn't like file URLs.
-               $xsdFile = __DIR__ . '/../../../../docs/sitelist-1.0.xsd';
-               $xsdData = file_get_contents( $xsdFile );
-
-               $document = new DOMDocument();
-               $document->loadXML( $xml, LIBXML_NONET );
-               $document->schemaValidateSource( $xsdData );
-       }
-
-       private function newSiteStore( SiteList $sites ) {
-               $store = $this->getMockBuilder( SiteStore::class )->getMock();
-
-               $store->expects( $this->once() )
-                       ->method( 'saveSites' )
-                       ->will( $this->returnCallback( function ( $moreSites ) use ( $sites ) {
-                               foreach ( $moreSites as $site ) {
-                                       $sites->setSite( $site );
-                               }
-                       } ) );
-
-               $store->expects( $this->any() )
-                       ->method( 'getSites' )
-                       ->will( $this->returnValue( new SiteList() ) );
-
-               return $store;
-       }
-
-       public function provideRoundTrip() {
-               $foo = Site::newForType( Site::TYPE_UNKNOWN );
-               $foo->setGlobalId( 'Foo' );
-
-               $acme = Site::newForType( Site::TYPE_UNKNOWN );
-               $acme->setGlobalId( 'acme.com' );
-               $acme->setGroup( 'Test' );
-               $acme->addLocalId( Site::ID_INTERWIKI, 'acme' );
-               $acme->setPath( Site::PATH_LINK, 'http://acme.com/' );
-
-               $dewiki = Site::newForType( Site::TYPE_MEDIAWIKI );
-               $dewiki->setGlobalId( 'dewiki' );
-               $dewiki->setGroup( 'wikipedia' );
-               $dewiki->setForward( true );
-               $dewiki->addLocalId( Site::ID_INTERWIKI, 'wikipedia' );
-               $dewiki->addLocalId( Site::ID_EQUIVALENT, 'de' );
-               $dewiki->setPath( Site::PATH_LINK, 'http://de.wikipedia.org/w/' );
-               $dewiki->setPath( MediaWikiSite::PATH_PAGE, 'http://de.wikipedia.org/wiki/' );
-               $dewiki->setSource( 'meta.wikimedia.org' );
-
-               return [
-                       'empty' => [
-                               new SiteList()
-                       ],
-
-                       'some' => [
-                               new SiteList( [ $foo, $acme, $dewiki ] ),
-                       ],
-               ];
-       }
-
-       /**
-        * @dataProvider provideRoundTrip()
-        */
-       public function testRoundTrip( SiteList $sites ) {
-               $tmp = tmpfile();
-               $exporter = new SiteExporter( $tmp );
-
-               $exporter->exportSites( $sites );
-
-               fseek( $tmp, 0 );
-               $xml = fread( $tmp, 16 * 1024 );
-
-               $actualSites = new SiteList();
-               $store = $this->newSiteStore( $actualSites );
-
-               $importer = new SiteImporter( $store );
-               $importer->importFromXML( $xml );
-
-               $this->assertEquals( $sites, $actualSites );
-       }
-
-}
diff --git a/tests/phpunit/includes/site/SiteImporterTest.php b/tests/phpunit/includes/site/SiteImporterTest.php
deleted file mode 100644 (file)
index dbdbd6f..0000000
+++ /dev/null
@@ -1,200 +0,0 @@
-<?php
-
-/**
- * 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; either version 2 of the License, or
- * (at your option) any later version.
- *
- * 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.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- *
- * @ingroup Site
- * @ingroup Test
- *
- * @group Site
- *
- * @covers SiteImporter
- *
- * @author Daniel Kinzler
- */
-class SiteImporterTest extends PHPUnit\Framework\TestCase {
-
-       use MediaWikiCoversValidator;
-       use PHPUnit4And6Compat;
-
-       private function newSiteImporter( array $expectedSites, $errorCount ) {
-               $store = $this->getMockBuilder( SiteStore::class )->getMock();
-
-               $store->expects( $this->once() )
-                       ->method( 'saveSites' )
-                       ->will( $this->returnCallback( function ( $sites ) use ( $expectedSites ) {
-                               $this->assertSitesEqual( $expectedSites, $sites );
-                       } ) );
-
-               $store->expects( $this->any() )
-                       ->method( 'getSites' )
-                       ->will( $this->returnValue( new SiteList() ) );
-
-               $errorHandler = $this->getMockBuilder( Psr\Log\LoggerInterface::class )->getMock();
-               $errorHandler->expects( $this->exactly( $errorCount ) )
-                       ->method( 'error' );
-
-               $importer = new SiteImporter( $store );
-               $importer->setExceptionCallback( [ $errorHandler, 'error' ] );
-
-               return $importer;
-       }
-
-       public function assertSitesEqual( $expected, $actual, $message = '' ) {
-               $this->assertEquals(
-                       $this->getSerializedSiteList( $expected ),
-                       $this->getSerializedSiteList( $actual ),
-                       $message
-               );
-       }
-
-       public function provideImportFromXML() {
-               $foo = Site::newForType( Site::TYPE_UNKNOWN );
-               $foo->setGlobalId( 'Foo' );
-
-               $acme = Site::newForType( Site::TYPE_UNKNOWN );
-               $acme->setGlobalId( 'acme.com' );
-               $acme->setGroup( 'Test' );
-               $acme->addLocalId( Site::ID_INTERWIKI, 'acme' );
-               $acme->setPath( Site::PATH_LINK, 'http://acme.com/' );
-
-               $dewiki = Site::newForType( Site::TYPE_MEDIAWIKI );
-               $dewiki->setGlobalId( 'dewiki' );
-               $dewiki->setGroup( 'wikipedia' );
-               $dewiki->setForward( true );
-               $dewiki->addLocalId( Site::ID_INTERWIKI, 'wikipedia' );
-               $dewiki->addLocalId( Site::ID_EQUIVALENT, 'de' );
-               $dewiki->setPath( Site::PATH_LINK, 'http://de.wikipedia.org/w/' );
-               $dewiki->setPath( MediaWikiSite::PATH_PAGE, 'http://de.wikipedia.org/wiki/' );
-               $dewiki->setSource( 'meta.wikimedia.org' );
-
-               return [
-                       'empty' => [
-                               '<sites></sites>',
-                               [],
-                       ],
-                       'no sites' => [
-                               '<sites><Foo><globalid>Foo</globalid></Foo><Bar><quux>Bla</quux></Bar></sites>',
-                               [],
-                       ],
-                       'minimal' => [
-                               '<sites>' .
-                                       '<site><globalid>Foo</globalid></site>' .
-                               '</sites>',
-                               [ $foo ],
-                       ],
-                       'full' => [
-                               '<sites>' .
-                                       '<site><globalid>Foo</globalid></site>' .
-                                       '<site>' .
-                                               '<globalid>acme.com</globalid>' .
-                                               '<localid type="interwiki">acme</localid>' .
-                                               '<group>Test</group>' .
-                                               '<path type="link">http://acme.com/</path>' .
-                                       '</site>' .
-                                       '<site type="mediawiki">' .
-                                               '<source>meta.wikimedia.org</source>' .
-                                               '<globalid>dewiki</globalid>' .
-                                               '<localid type="interwiki">wikipedia</localid>' .
-                                               '<localid type="equivalent">de</localid>' .
-                                               '<group>wikipedia</group>' .
-                                               '<forward/>' .
-                                               '<path type="link">http://de.wikipedia.org/w/</path>' .
-                                               '<path type="page_path">http://de.wikipedia.org/wiki/</path>' .
-                                       '</site>' .
-                               '</sites>',
-                               [ $foo, $acme, $dewiki ],
-                       ],
-                       'skip' => [
-                               '<sites>' .
-                                       '<site><globalid>Foo</globalid></site>' .
-                                       '<site><barf>Foo</barf></site>' .
-                                       '<site>' .
-                                               '<globalid>acme.com</globalid>' .
-                                               '<localid type="interwiki">acme</localid>' .
-                                               '<silly>boop!</silly>' .
-                                               '<group>Test</group>' .
-                                               '<path type="link">http://acme.com/</path>' .
-                                       '</site>' .
-                               '</sites>',
-                               [ $foo, $acme ],
-                               1
-                       ],
-               ];
-       }
-
-       /**
-        * @dataProvider provideImportFromXML
-        */
-       public function testImportFromXML( $xml, array $expectedSites, $errorCount = 0 ) {
-               $importer = $this->newSiteImporter( $expectedSites, $errorCount );
-               $importer->importFromXML( $xml );
-       }
-
-       public function testImportFromXML_malformed() {
-               $this->setExpectedException( Exception::class );
-
-               $store = $this->getMockBuilder( SiteStore::class )->getMock();
-               $importer = new SiteImporter( $store );
-               $importer->importFromXML( 'THIS IS NOT XML' );
-       }
-
-       public function testImportFromFile() {
-               $foo = Site::newForType( Site::TYPE_UNKNOWN );
-               $foo->setGlobalId( 'Foo' );
-
-               $acme = Site::newForType( Site::TYPE_UNKNOWN );
-               $acme->setGlobalId( 'acme.com' );
-               $acme->setGroup( 'Test' );
-               $acme->addLocalId( Site::ID_INTERWIKI, 'acme' );
-               $acme->setPath( Site::PATH_LINK, 'http://acme.com/' );
-
-               $dewiki = Site::newForType( Site::TYPE_MEDIAWIKI );
-               $dewiki->setGlobalId( 'dewiki' );
-               $dewiki->setGroup( 'wikipedia' );
-               $dewiki->setForward( true );
-               $dewiki->addLocalId( Site::ID_INTERWIKI, 'wikipedia' );
-               $dewiki->addLocalId( Site::ID_EQUIVALENT, 'de' );
-               $dewiki->setPath( Site::PATH_LINK, 'http://de.wikipedia.org/w/' );
-               $dewiki->setPath( MediaWikiSite::PATH_PAGE, 'http://de.wikipedia.org/wiki/' );
-               $dewiki->setSource( 'meta.wikimedia.org' );
-
-               $importer = $this->newSiteImporter( [ $foo, $acme, $dewiki ], 0 );
-
-               $file = __DIR__ . '/SiteImporterTest.xml';
-               $importer->importFromFile( $file );
-       }
-
-       /**
-        * @param Site[] $sites
-        *
-        * @return array[]
-        */
-       private function getSerializedSiteList( $sites ) {
-               $serialized = [];
-
-               foreach ( $sites as $site ) {
-                       $key = $site->getGlobalId();
-                       $data = unserialize( $site->serialize() );
-
-                       $serialized[$key] = $data;
-               }
-
-               return $serialized;
-       }
-}
diff --git a/tests/phpunit/includes/site/SiteImporterTest.xml b/tests/phpunit/includes/site/SiteImporterTest.xml
deleted file mode 100644 (file)
index 720b1fa..0000000
+++ /dev/null
@@ -1,19 +0,0 @@
-<sites version="1.0" xmlns="http://www.mediawiki.org/xml/sitelist-1.0/">
-       <site><globalid>Foo</globalid></site>
-       <site>
-               <globalid>acme.com</globalid>
-               <localid type="interwiki">acme</localid>
-               <group>Test</group>
-               <path type="link">http://acme.com/</path>
-       </site>
-       <site type="mediawiki">
-               <source>meta.wikimedia.org</source>
-               <globalid>dewiki</globalid>
-               <localid type="interwiki">wikipedia</localid>
-               <localid type="equivalent">de</localid>
-               <group>wikipedia</group>
-               <forward/>
-               <path type="link">http://de.wikipedia.org/w/</path>
-               <path type="page_path">http://de.wikipedia.org/wiki/</path>
-       </site>
-</sites>
diff --git a/tests/phpunit/includes/skins/SkinFactoryTest.php b/tests/phpunit/includes/skins/SkinFactoryTest.php
deleted file mode 100644 (file)
index 4289fd9..0000000
+++ /dev/null
@@ -1,82 +0,0 @@
-<?php
-
-class SkinFactoryTest extends MediaWikiTestCase {
-
-       /**
-        * @covers SkinFactory::register
-        */
-       public function testRegister() {
-               $factory = new SkinFactory();
-               $factory->register( 'fallback', 'Fallback', function () {
-                       return new SkinFallback();
-               } );
-               $this->assertTrue( true ); // No exception thrown
-               $this->setExpectedException( InvalidArgumentException::class );
-               $factory->register( 'invalid', 'Invalid', 'Invalid callback' );
-       }
-
-       /**
-        * @covers SkinFactory::makeSkin
-        */
-       public function testMakeSkinWithNoBuilders() {
-               $factory = new SkinFactory();
-               $this->setExpectedException( SkinException::class );
-               $factory->makeSkin( 'nobuilderregistered' );
-       }
-
-       /**
-        * @covers SkinFactory::makeSkin
-        */
-       public function testMakeSkinWithInvalidCallback() {
-               $factory = new SkinFactory();
-               $factory->register( 'unittest', 'Unittest', function () {
-                       return true; // Not a Skin object
-               } );
-               $this->setExpectedException( UnexpectedValueException::class );
-               $factory->makeSkin( 'unittest' );
-       }
-
-       /**
-        * @covers SkinFactory::makeSkin
-        */
-       public function testMakeSkinWithValidCallback() {
-               $factory = new SkinFactory();
-               $factory->register( 'testfallback', 'TestFallback', function () {
-                       return new SkinFallback();
-               } );
-
-               $skin = $factory->makeSkin( 'testfallback' );
-               $this->assertInstanceOf( Skin::class, $skin );
-               $this->assertInstanceOf( SkinFallback::class, $skin );
-               $this->assertEquals( 'fallback', $skin->getSkinName() );
-       }
-
-       /**
-        * @covers Skin::__construct
-        * @covers Skin::getSkinName
-        */
-       public function testGetSkinName() {
-               $skin = new SkinFallback();
-               $this->assertEquals( 'fallback', $skin->getSkinName(), 'Default' );
-               $skin = new SkinFallback( 'testname' );
-               $this->assertEquals( 'testname', $skin->getSkinName(), 'Constructor argument' );
-       }
-
-       /**
-        * @covers SkinFactory::getSkinNames
-        */
-       public function testGetSkinNames() {
-               $factory = new SkinFactory();
-               // A fake callback we can use that will never be called
-               $callback = function () {
-                       // NOP
-               };
-               $factory->register( 'skin1', 'Skin1', $callback );
-               $factory->register( 'skin2', 'Skin2', $callback );
-               $names = $factory->getSkinNames();
-               $this->assertArrayHasKey( 'skin1', $names );
-               $this->assertArrayHasKey( 'skin2', $names );
-               $this->assertEquals( 'Skin1', $names['skin1'] );
-               $this->assertEquals( 'Skin2', $names['skin2'] );
-       }
-}
diff --git a/tests/phpunit/includes/skins/SkinTemplateTest.php b/tests/phpunit/includes/skins/SkinTemplateTest.php
deleted file mode 100644 (file)
index 6ea5b40..0000000
+++ /dev/null
@@ -1,109 +0,0 @@
-<?php
-
-/**
- * @covers SkinTemplate
- *
- * @group Output
- *
- * @author Bene* < benestar.wikimedia@gmail.com >
- */
-class SkinTemplateTest extends MediaWikiTestCase {
-       /**
-        * @dataProvider makeListItemProvider
-        */
-       public function testMakeListItem( $expected, $key, $item, $options, $message ) {
-               $template = $this->getMockForAbstractClass( BaseTemplate::class );
-
-               $this->assertEquals(
-                       $expected,
-                       $template->makeListItem( $key, $item, $options ),
-                       $message
-               );
-       }
-
-       public function makeListItemProvider() {
-               return [
-                       [
-                               '<li class="class" title="itemtitle"><a href="url" title="title">text</a></li>',
-                               '',
-                               [
-                                       'class' => 'class',
-                                       'itemtitle' => 'itemtitle',
-                                       'href' => 'url',
-                                       'title' => 'title',
-                                       'text' => 'text'
-                               ],
-                               [],
-                               'Test makeListItem with normal values'
-                       ]
-               ];
-       }
-
-       /**
-        * @return PHPUnit_Framework_MockObject_MockObject|OutputPage
-        */
-       private function getMockOutputPage( $isSyndicated, $html ) {
-               $mock = $this->getMockBuilder( OutputPage::class )
-                       ->disableOriginalConstructor()
-                       ->getMock();
-               $mock->expects( $this->once() )
-                       ->method( 'isSyndicated' )
-                       ->will( $this->returnValue( $isSyndicated ) );
-               $mock->expects( $this->any() )
-                       ->method( 'getHTML' )
-                       ->will( $this->returnValue( $html ) );
-               return $mock;
-       }
-
-       public function provideGetDefaultModules() {
-               $defaultStyles = [
-                       'mediawiki.legacy.shared',
-                       'mediawiki.legacy.commonPrint',
-               ];
-               $buttonStyle = 'mediawiki.ui.button';
-               $feedStyle = 'mediawiki.feedlink';
-               return [
-                       [
-                               false,
-                               '',
-                               $defaultStyles
-                       ],
-                       [
-                               true,
-                               '',
-                               array_merge( $defaultStyles, [ $feedStyle ] )
-                       ],
-                       [
-                               false,
-                               'FOO mw-ui-button BAR',
-                               array_merge( $defaultStyles, [ $buttonStyle ] )
-                       ],
-                       [
-                               true,
-                               'FOO mw-ui-button BAR',
-                               array_merge( $defaultStyles, [ $buttonStyle, $feedStyle ] )
-                       ],
-               ];
-       }
-
-       /**
-        * @covers Skin::getDefaultModules
-        * @dataProvider provideGetDefaultModules
-        */
-       public function testgetDefaultModules( $isSyndicated, $html, $expectedModuleStyles ) {
-               $skin = new SkinTemplate();
-
-               $context = new DerivativeContext( $skin->getContext() );
-               $context->setOutput( $this->getMockOutputPage( $isSyndicated, $html ) );
-               $skin->setContext( $context );
-
-               $modules = $skin->getDefaultModules();
-
-               $actualStylesModule = call_user_func_array( 'array_merge', $modules['styles'] );
-               $this->assertArraySubset(
-                       $expectedModuleStyles,
-                       $actualStylesModule,
-                       'style modules'
-               );
-       }
-}
diff --git a/tests/phpunit/includes/skins/SkinTest.php b/tests/phpunit/includes/skins/SkinTest.php
deleted file mode 100644 (file)
index 41ef2b7..0000000
+++ /dev/null
@@ -1,16 +0,0 @@
-<?php
-class SkinTest extends MediaWikiTestCase {
-
-       /**
-        * @covers Skin::getDefaultModules
-        */
-       public function testGetDefaultModules() {
-               $skin = $this->getMockBuilder( Skin::class )
-                       ->setMethods( [ 'outputPage', 'setupSkinUserCss' ] )
-                       ->getMock();
-
-               $modules = $skin->getDefaultModules();
-               $this->assertTrue( isset( $modules['core'] ), 'core key is set by default' );
-               $this->assertTrue( isset( $modules['styles'] ), 'style key is set by default' );
-       }
-}
diff --git a/tests/phpunit/includes/sparql/SparqlClientTest.php b/tests/phpunit/includes/sparql/SparqlClientTest.php
deleted file mode 100644 (file)
index 62af489..0000000
+++ /dev/null
@@ -1,191 +0,0 @@
-<?php
-
-namespace MediaWiki\Sparql;
-
-use Http;
-use MediaWiki\Http\HttpRequestFactory;
-use MWHttpRequest;
-use PHPUnit4And6Compat;
-
-/**
- * @covers \MediaWiki\Sparql\SparqlClient
- */
-class SparqlClientTest extends \PHPUnit\Framework\TestCase {
-
-       use PHPUnit4And6Compat;
-
-       private function getRequestFactory( $request ) {
-               $requestFactory = $this->getMock( HttpRequestFactory::class );
-               $requestFactory->method( 'create' )->willReturn( $request );
-               return $requestFactory;
-       }
-
-       private function getRequestMock( $content ) {
-               $request = $this->getMockBuilder( MWHttpRequest::class )->disableOriginalConstructor()->getMock();
-               $request->method( 'execute' )->willReturn( \Status::newGood( 200 ) );
-               $request->method( 'getContent' )->willReturn( $content );
-               return $request;
-       }
-
-       public function testQuery() {
-               $json = <<<JSON
-{
-  "head" : {
-    "vars" : [ "x", "y", "z" ]
-  },
-  "results" : {
-    "bindings" : [ {
-      "x" : {
-        "type" : "uri",
-        "value" : "http://wikiba.se/ontology#Dump"
-      },
-      "y" : {
-        "type" : "uri",
-        "value" : "http://creativecommons.org/ns#license"
-      },
-      "z" : {
-        "type" : "uri",
-        "value" : "http://creativecommons.org/publicdomain/zero/1.0/"
-      }
-    }, {
-      "x" : {
-        "type" : "uri",
-        "value" : "http://wikiba.se/ontology#Dump"
-      },
-      "z" : {
-        "type" : "literal",
-        "value" : "0.1.0"
-      }
-    } ]
-  }
-}
-JSON;
-
-               $request = $this->getRequestMock( $json );
-               $client = new SparqlClient( 'http://acme.test/', $this->getRequestFactory( $request ) );
-
-               // values only
-               $result = $client->query( "TEST SPARQL" );
-               $this->assertCount( 2, $result );
-               $this->assertEquals( 'http://wikiba.se/ontology#Dump', $result[0]['x'] );
-               $this->assertEquals( 'http://creativecommons.org/ns#license', $result[0]['y'] );
-               $this->assertEquals( '0.1.0', $result[1]['z'] );
-               $this->assertNull( $result[1]['y'] );
-               // raw data format
-               $result = $client->query( "TEST SPARQL 2", true );
-               $this->assertCount( 2, $result );
-               $this->assertEquals( 'uri', $result[0]['x']['type'] );
-               $this->assertEquals( 'http://wikiba.se/ontology#Dump', $result[0]['x']['value'] );
-               $this->assertEquals( 'literal', $result[1]['z']['type'] );
-               $this->assertEquals( '0.1.0', $result[1]['z']['value'] );
-               $this->assertNull( $result[1]['y'] );
-       }
-
-       /**
-        * @expectedException \Mediawiki\Sparql\SparqlException
-        */
-       public function testBadQuery() {
-               $request = $this->getMockBuilder( MWHttpRequest::class )->disableOriginalConstructor()->getMock();
-               $client = new SparqlClient( 'http://acme.test/', $this->getRequestFactory( $request ) );
-
-               $request->method( 'execute' )->willReturn( \Status::newFatal( "Bad query" ) );
-               $result = $client->query( "TEST SPARQL 3" );
-       }
-
-       public function optionsProvider() {
-               return [
-                       'defaults' => [
-                               'TEST тест SPARQL 4 ',
-                               null,
-                               null,
-                               [
-                                       'http://acme.test/',
-                                       'query=TEST+%D1%82%D0%B5%D1%81%D1%82+SPARQL+4+',
-                                       'format=json',
-                                       'maxQueryTimeMillis=30000',
-                               ],
-                               [
-                                       'method' => 'GET',
-                                       'userAgent' => Http::userAgent() . " SparqlClient",
-                                       'timeout' => 30
-                               ]
-                       ],
-                       'big query' => [
-                               str_repeat( 'ZZ', SparqlClient::MAX_GET_SIZE ),
-                               null,
-                               null,
-                               [
-                                       'format=json',
-                                       'maxQueryTimeMillis=30000',
-                               ],
-                               [
-                                       'method' => 'POST',
-                                       'postData' => 'query=' . str_repeat( 'ZZ', SparqlClient::MAX_GET_SIZE ),
-                               ]
-                       ],
-                       'timeout 1s' => [
-                               'TEST SPARQL 4',
-                               null,
-                               1,
-                               [
-                                       'maxQueryTimeMillis=1000',
-                               ],
-                               [
-                                       'timeout' => 1
-                               ]
-                       ],
-                       'more options' => [
-                               'TEST SPARQL 5',
-                               [
-                                       'userAgent' => 'My Test',
-                                       'randomOption' => 'duck',
-                               ],
-                               null,
-                               [],
-                               [
-                                       'userAgent' => 'My Test',
-                                       'randomOption' => 'duck',
-                               ]
-                       ],
-
-               ];
-       }
-
-       /**
-        * @dataProvider  optionsProvider
-        * @param string $sparql
-        * @param array|null $options
-        * @param int|null $timeout
-        * @param array $expectedUrl
-        * @param array $expectedOptions
-        */
-       public function testOptions( $sparql, $options, $timeout, $expectedUrl, $expectedOptions ) {
-               $requestFactory = $this->getMock( HttpRequestFactory::class );
-               $client = new SparqlClient( 'http://acme.test/',  $requestFactory );
-
-               $request = $this->getRequestMock( '{}' );
-
-               $requestFactory->method( 'create' )->willReturnCallback(
-                       function ( $url, $options ) use ( $request, $expectedUrl, $expectedOptions ) {
-                               foreach ( $expectedUrl as $eurl ) {
-                                       $this->assertContains( $eurl, $url );
-                               }
-                               foreach ( $expectedOptions as $ekey => $evalue ) {
-                                       $this->assertArrayHasKey( $ekey, $options );
-                                       $this->assertEquals( $options[$ekey], $evalue );
-                               }
-                               return $request;
-                       }
-               );
-
-               if ( !is_null( $options ) ) {
-                       $client->setClientOptions( $options );
-               }
-               if ( !is_null( $timeout ) ) {
-                       $client->setTimeout( $timeout );
-               }
-
-               $result = $client->query( $sparql );
-       }
-
-}
diff --git a/tests/phpunit/includes/specials/ImageListPagerTest.php b/tests/phpunit/includes/specials/ImageListPagerTest.php
deleted file mode 100644 (file)
index 10c6d04..0000000
+++ /dev/null
@@ -1,21 +0,0 @@
-<?php
-/**
- * Test class for ImageListPagerTest class.
- *
- * Copyright © 2013, Antoine Musso
- * Copyright © 2013, Siebrand Mazeland
- * Copyright © 2013, Wikimedia Foundation Inc.
- *
- * @group Database
- */
-class ImageListPagerTest extends MediaWikiTestCase {
-       /**
-        * @expectedException MWException
-        * @expectedExceptionMessage invalid_field
-        * @covers ImageListPager::formatValue
-        */
-       public function testFormatValuesThrowException() {
-               $page = new ImageListPager( RequestContext::getMain() );
-               $page->formatValue( 'invalid_field', 'invalid_value' );
-       }
-}
diff --git a/tests/phpunit/includes/specials/SpecialUploadTest.php b/tests/phpunit/includes/specials/SpecialUploadTest.php
deleted file mode 100644 (file)
index 95026c1..0000000
+++ /dev/null
@@ -1,29 +0,0 @@
-<?php
-
-class SpecialUploadTest extends MediaWikiTestCase {
-       /**
-        * @covers SpecialUpload::getInitialPageText
-        * @dataProvider provideGetInitialPageText
-        */
-       public function testGetInitialPageText( $expected, $inputParams ) {
-               $result = call_user_func_array( [ 'SpecialUpload', 'getInitialPageText' ], $inputParams );
-               $this->assertEquals( $expected, $result );
-       }
-
-       public function provideGetInitialPageText() {
-               return [
-                       [
-                               'expect' => "== Summary ==\nthis is a test\n",
-                               'params' => [
-                                       'this is a test'
-                               ],
-                       ],
-                       [
-                               'expect' => "== Summary ==\nthis is a test\n",
-                               'params' => [
-                                       "== Summary ==\nthis is a test",
-                               ],
-                       ],
-               ];
-       }
-}
diff --git a/tests/phpunit/includes/specials/UncategorizedCategoriesPageTest.php b/tests/phpunit/includes/specials/UncategorizedCategoriesPageTest.php
deleted file mode 100644 (file)
index 80bd365..0000000
+++ /dev/null
@@ -1,63 +0,0 @@
-<?php
-/**
- * Tests for Special:Uncategorizedcategories
- */
-class UncategorizedCategoriesPageTest extends MediaWikiTestCase {
-       /**
-        * @dataProvider provideTestGetQueryInfoData
-        * @covers UncategorizedCategoriesPage::getQueryInfo
-        */
-       public function testGetQueryInfo( $msgContent, $expected ) {
-               $msg = new RawMessage( $msgContent );
-               $mockContext = $this->getMockBuilder( RequestContext::class )->getMock();
-               $mockContext->method( 'msg' )->willReturn( $msg );
-               $special = new UncategorizedCategoriesPage();
-               $special->setContext( $mockContext );
-               $this->assertEquals( [
-                       'tables' => [
-                               0 => 'page',
-                               1 => 'categorylinks',
-                       ],
-                       'fields' => [
-                               'namespace' => 'page_namespace',
-                               'title' => 'page_title',
-                               'value' => 'page_title',
-                       ],
-                       'conds' => [
-                               0 => 'cl_from IS NULL',
-                               'page_namespace' => 14,
-                               'page_is_redirect' => 0,
-                       ] + $expected,
-                       'join_conds' => [
-                               'categorylinks' => [
-                                       0 => 'LEFT JOIN',
-                                       1 => 'cl_from = page_id',
-                               ],
-                       ],
-               ], $special->getQueryInfo() );
-       }
-
-       public function provideTestGetQueryInfoData() {
-               return [
-                       [
-                               "* Stubs\n* Test\n* *\n* * test123",
-                               [ 1 => "page_title not in ( 'Stubs','Test','*','*_test123' )" ]
-                       ],
-                       [
-                               "Stubs\n* Test\n* *\n* * test123",
-                               [ 1 => "page_title not in ( 'Test','*','*_test123' )" ]
-                       ],
-                       [
-                               "* StubsTest\n* *\n* * test123",
-                               [ 1 => "page_title not in ( 'StubsTest','*','*_test123' )" ]
-                       ],
-                       [ "", [] ],
-                       [ "\n\n\n", [] ],
-                       [ "\n", [] ],
-                       [ "Test\n*Test2", [ 1 => "page_title not in ( 'Test2' )" ] ],
-                       [ "Test", [] ],
-                       [ "*Test\nTest2", [ 1 => "page_title not in ( 'Test' )" ] ],
-                       [ "Test\nTest2", [] ],
-               ];
-       }
-}
diff --git a/tests/phpunit/includes/tidy/RemexDriverTest.php b/tests/phpunit/includes/tidy/RemexDriverTest.php
deleted file mode 100644 (file)
index 5ad8416..0000000
+++ /dev/null
@@ -1,326 +0,0 @@
-<?php
-
-class RemexDriverTest extends MediaWikiTestCase {
-       private static $remexTidyTestData = [
-               [
-                       'Empty string',
-                       "",
-                       ""
-               ],
-               [
-                       'Simple p-wrap',
-                       "x",
-                       "<p>x</p>"
-               ],
-               [
-                       'No p-wrap of blank node',
-                       " ",
-                       " "
-               ],
-               [
-                       'p-wrap terminated by div',
-                       "x<div></div>",
-                       "<p>x</p><div></div>"
-               ],
-               [
-                       'p-wrap not terminated by span',
-                       "x<span></span>",
-                       "<p>x<span></span></p>"
-               ],
-               [
-                       'An element is non-blank and so gets p-wrapped',
-                       "<span></span>",
-                       "<p><span></span></p>"
-               ],
-               [
-                       'The blank flag is set after a block-level element',
-                       "<div></div> ",
-                       "<div></div> "
-               ],
-               [
-                       'Blank detection between two block-level elements',
-                       "<div></div> <div></div>",
-                       "<div></div> <div></div>"
-               ],
-               [
-                       'But p-wrapping of non-blank content works after an element',
-                       "<div></div>x",
-                       "<div></div><p>x</p>"
-               ],
-               [
-                       'p-wrapping between two block-level elements',
-                       "<div></div>x<div></div>",
-                       "<div></div><p>x</p><div></div>"
-               ],
-               [
-                       'p-wrap inside blockquote',
-                       "<blockquote>x</blockquote>",
-                       "<blockquote><p>x</p></blockquote>"
-               ],
-               [
-                       'A comment is blank for p-wrapping purposes',
-                       "<!-- x -->",
-                       "<!-- x -->"
-               ],
-               [
-                       'A comment is blank even when a p-wrap was opened by a text node',
-                       " <!-- x -->",
-                       " <!-- x -->"
-               ],
-               [
-                       'A comment does not open a p-wrap',
-                       "<!-- x -->x",
-                       "<!-- x --><p>x</p>"
-               ],
-               [
-                       'A comment does not close a p-wrap',
-                       "x<!-- x -->",
-                       "<p>x<!-- x --></p>"
-               ],
-               [
-                       'Empty li',
-                       "<ul><li></li></ul>",
-                       "<ul><li class=\"mw-empty-elt\"></li></ul>"
-               ],
-               [
-                       'li with element',
-                       "<ul><li><span></span></li></ul>",
-                       "<ul><li><span></span></li></ul>"
-               ],
-               [
-                       'li with text',
-                       "<ul><li>x</li></ul>",
-                       "<ul><li>x</li></ul>"
-               ],
-               [
-                       'Empty tr',
-                       "<table><tbody><tr></tr></tbody></table>",
-                       "<table><tbody><tr class=\"mw-empty-elt\"></tr></tbody></table>"
-               ],
-               [
-                       'Empty p',
-                       "<p>\n</p>",
-                       "<p class=\"mw-empty-elt\">\n</p>"
-               ],
-               [
-                       'No p-wrapping of an inline element which contains a block element (T150317)',
-                       "<small><div>x</div></small>",
-                       "<small><div>x</div></small>"
-               ],
-               [
-                       'p-wrapping of an inline element which contains an inline element',
-                       "<small><b>x</b></small>",
-                       "<p><small><b>x</b></small></p>"
-               ],
-               [
-                       'p-wrapping is enabled in a blockquote in an inline element',
-                       "<small><blockquote>x</blockquote></small>",
-                       "<small><blockquote><p>x</p></blockquote></small>"
-               ],
-               [
-                       'All bare text should be p-wrapped even when surrounded by block tags',
-                       "<small><blockquote>x</blockquote></small>y<div></div>z",
-                       "<small><blockquote><p>x</p></blockquote></small><p>y</p><div></div><p>z</p>"
-               ],
-               [
-                       'Split tag stack 1',
-                       "<small>x<div>y</div>z</small>",
-                       "<p><small>x</small></p><small><div>y</div></small><p><small>z</small></p>"
-               ],
-               [
-                       'Split tag stack 2',
-                       "<small><div>y</div>z</small>",
-                       "<small><div>y</div></small><p><small>z</small></p>"
-               ],
-               [
-                       'Split tag stack 3',
-                       "<small>x<div>y</div></small>",
-                       "<p><small>x</small></p><small><div>y</div></small>"
-               ],
-               [
-                       'Split tag stack 4 (modified to use splittable tag)',
-                       "a<code>b<i>c<div>d</div></i>e</code>",
-                       "<p>a<code>b<i>c</i></code></p><code><i><div>d</div></i></code><p><code>e</code></p>"
-               ],
-               [
-                       "Split tag stack regression check 1",
-                       "x<span><div>y</div></span>",
-                       "<p>x</p><span><div>y</div></span>"
-               ],
-               [
-                       "Split tag stack regression check 2 (modified to use splittable tag)",
-                       "a<code><i><div>d</div></i>e</code>",
-                       "<p>a</p><code><i><div>d</div></i></code><p><code>e</code></p>"
-               ],
-               // Simple tests from pwrap.js
-               [
-                       'Simple pwrap test 1',
-                       'a',
-                       '<p>a</p>'
-               ],
-               [
-                       '<span> is not a splittable tag, but gets p-wrapped in simple wrapping scenarios',
-                       '<span>a</span>',
-                       '<p><span>a</span></p>'
-               ],
-               [
-                       'Simple pwrap test 3',
-                       'x <div>a</div> <div>b</div> y',
-                       '<p>x </p><div>a</div> <div>b</div><p> y</p>'
-               ],
-               [
-                       'Simple pwrap test 4',
-                       'x<!--c--> <div>a</div> <div>b</div> <!--c-->y',
-                       '<p>x<!--c--> </p><div>a</div> <div>b</div> <!--c--><p>y</p>'
-               ],
-               // Complex tests from pwrap.js
-               [
-                       'Complex pwrap test 1',
-                       '<i>x<div>a</div>y</i>',
-                       '<p><i>x</i></p><i><div>a</div></i><p><i>y</i></p>'
-               ],
-               [
-                       'Complex pwrap test 2',
-                       'a<small>b</small><i>c<div>d</div>e</i>f',
-                       '<p>a<small>b</small><i>c</i></p><i><div>d</div></i><p><i>e</i>f</p>'
-               ],
-               [
-                       'Complex pwrap test 3',
-                       'a<small>b<i>c<div>d</div></i>e</small>',
-                       '<p>a<small>b<i>c</i></small></p><small><i><div>d</div></i></small><p><small>e</small></p>'
-               ],
-               [
-                       'Complex pwrap test 4',
-                       'x<small><div>y</div></small>',
-                       '<p>x</p><small><div>y</div></small>'
-               ],
-               [
-                       'Complex pwrap test 5',
-                       'a<small><i><div>d</div></i>e</small>',
-                       '<p>a</p><small><i><div>d</div></i></small><p><small>e</small></p>'
-               ],
-               // phpcs:disable Generic.Files.LineLength
-               [
-                       'Complex pwrap test 6',
-                       '<i>a<div>b</div>c<b>d<div>e</div>f</b>g</i>',
-                       // PHP 5 does not allow concatenation in initialisation of a class static variable
-                       '<p><i>a</i></p><i><div>b</div></i><p><i>c<b>d</b></i></p><i><b><div>e</div></b></i><p><i><b>f</b>g</i></p>'
-               ],
-               // phpcs:enable
-               /* FIXME the second <b> causes a stack split which clones the <i> even
-                * though no <p> is actually generated
-               [
-                       'Complex pwrap test 7',
-                       '<i><b><font><div>x</div></font></b><div>y</div><b><font><div>z</div></font></b></i>',
-                       '<i><b><font><div>x</div></font></b><div>y</div><b><font><div>z</div></font></b></i>'
-               ],
-                */
-               // New local tests
-               [
-                       'Blank text node after block end',
-                       '<small>x<div>y</div> <b>z</b></small>',
-                       '<p><small>x</small></p><small><div>y</div></small><p><small> <b>z</b></small></p>'
-               ],
-               [
-                       'Text node fostering (FIXME: wrap missing)',
-                       '<table>x</table>',
-                       'x<table></table>'
-               ],
-               [
-                       'Blockquote fostering',
-                       '<table><blockquote>x</blockquote></table>',
-                       '<blockquote><p>x</p></blockquote><table></table>'
-               ],
-               [
-                       'Block element fostering',
-                       '<table><div>x',
-                       '<div>x</div><table></table>'
-               ],
-               [
-                       'Formatting element fostering (FIXME: wrap missing)',
-                       '<table><b>x',
-                       '<b>x</b><table></table>'
-               ],
-               [
-                       'AAA clone of p-wrapped element (FIXME: empty b)',
-                       '<b>x<p>y</b>z</p>',
-                       '<p><b>x</b></p><b></b><p><b>y</b>z</p>',
-               ],
-               [
-                       'AAA with fostering (FIXME: wrap missing)',
-                       '<table><b>1<p>2</b>3</p>',
-                       '<b>1</b><p><b>2</b>3</p><table></table>'
-               ],
-               [
-                       'AAA causes reparent of p-wrapped text node (T178632)',
-                       '<i><blockquote>x</i></blockquote>',
-                       '<i></i><blockquote><p><i>x</i></p></blockquote>',
-               ],
-               [
-                       'p-wrap ended by reparenting (T200827)',
-                       '<i><blockquote><p></i>',
-                       '<i></i><blockquote><p><i></i></p><p><i></i></p></blockquote>',
-               ],
-               [
-                       'style tag isn\'t p-wrapped (T186965)',
-                       '<style>/* ... */</style>',
-                       '<style>/* ... */</style>',
-               ],
-               [
-                       'link tag isn\'t p-wrapped (T186965)',
-                       '<link rel="foo" href="bar" />',
-                       '<link rel="foo" href="bar" />',
-               ],
-               [
-                       'style tag doesn\'t split p-wrapping (T208901)',
-                       'foo <style>/* ... */</style> bar',
-                       '<p>foo <style>/* ... */</style> bar</p>',
-               ],
-               [
-                       'link tag doesn\'t split p-wrapping (T208901)',
-                       'foo <link rel="foo" href="bar" /> bar',
-                       '<p>foo <link rel="foo" href="bar" /> bar</p>',
-               ],
-       ];
-
-       public function provider() {
-               return self::$remexTidyTestData;
-       }
-
-       /**
-        * @dataProvider provider
-        * @covers MediaWiki\Tidy\RemexCompatFormatter
-        * @covers MediaWiki\Tidy\RemexCompatMunger
-        * @covers MediaWiki\Tidy\RemexDriver
-        * @covers MediaWiki\Tidy\RemexMungerData
-        */
-       public function testTidy( $desc, $input, $expected ) {
-               $r = new MediaWiki\Tidy\RemexDriver( [] );
-               $result = $r->tidy( $input );
-               $this->assertEquals( $expected, $result, $desc );
-       }
-
-       public function html5libProvider() {
-               $files = json_decode( file_get_contents( __DIR__ . '/html5lib-tests.json' ), true );
-               $tests = [];
-               foreach ( $files as $file => $fileTests ) {
-                       foreach ( $fileTests as $i => $test ) {
-                               $tests[] = [ "$file:$i", $test['data'] ];
-                       }
-               }
-               return $tests;
-       }
-
-       /**
-        * This is a quick and dirty test to make sure none of the html5lib tests
-        * generate exceptions. We don't really know what the expected output is.
-        *
-        * @dataProvider html5libProvider
-        * @coversNothing
-        */
-       public function testHtml5Lib( $desc, $input ) {
-               $r = new MediaWiki\Tidy\RemexDriver( [] );
-               $result = $r->tidy( $input );
-               $this->assertTrue( true, $desc );
-       }
-}
diff --git a/tests/phpunit/includes/tidy/html5lib-tests.json b/tests/phpunit/includes/tidy/html5lib-tests.json
deleted file mode 100644 (file)
index 2b1c3e8..0000000
+++ /dev/null
@@ -1,80692 +0,0 @@
-{
-  "adoption01.dat": [
-    {
-      "data": "<a><p></a></p>",
-      "errors": [
-        "(1,3): expected-doctype-but-got-start-tag",
-        "(1,10): adoption-agency-1.3"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "a": true,
-            "p": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "a"
-                  },
-                  {
-                    "tag": "p",
-                    "children": [
-                      {
-                        "tag": "a"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><a></a><p><a></a></p></body></html>",
-        "noQuirksBodyHtml": "<a></a><p><a></a></p>"
-      }
-    },
-    {
-      "data": "<a>1<p>2</a>3</p>",
-      "errors": [
-        "(1,3): expected-doctype-but-got-start-tag",
-        "(1,12): adoption-agency-1.3"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "a": true,
-            "p": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "a",
-                    "children": [
-                      {
-                        "text": "1"
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "p",
-                    "children": [
-                      {
-                        "tag": "a",
-                        "children": [
-                          {
-                            "text": "2"
-                          }
-                        ]
-                      },
-                      {
-                        "text": "3"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><a>1</a><p><a>2</a>3</p></body></html>",
-        "noQuirksBodyHtml": "<a>1</a><p><a>2</a>3</p>"
-      }
-    },
-    {
-      "data": "<a>1<button>2</a>3</button>",
-      "errors": [
-        "(1,3): expected-doctype-but-got-start-tag",
-        "(1,17): adoption-agency-1.3"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "a": true,
-            "button": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "a",
-                    "children": [
-                      {
-                        "text": "1"
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "button",
-                    "children": [
-                      {
-                        "tag": "a",
-                        "children": [
-                          {
-                            "text": "2"
-                          }
-                        ]
-                      },
-                      {
-                        "text": "3"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><a>1</a><button><a>2</a>3</button></body></html>",
-        "noQuirksBodyHtml": "<a>1</a><button><a>2</a>3</button>"
-      }
-    },
-    {
-      "data": "<a>1<b>2</a>3</b>",
-      "errors": [
-        "(1,3): expected-doctype-but-got-start-tag",
-        "(1,12): adoption-agency-1.3"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "a": true,
-            "b": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "a",
-                    "children": [
-                      {
-                        "text": "1"
-                      },
-                      {
-                        "tag": "b",
-                        "children": [
-                          {
-                            "text": "2"
-                          }
-                        ]
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "b",
-                    "children": [
-                      {
-                        "text": "3"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><a>1<b>2</b></a><b>3</b></body></html>",
-        "noQuirksBodyHtml": "<a>1<b>2</b></a><b>3</b>"
-      }
-    },
-    {
-      "data": "<a>1<div>2<div>3</a>4</div>5</div>",
-      "errors": [
-        "(1,3): expected-doctype-but-got-start-tag",
-        "(1,20): adoption-agency-1.3",
-        "(1,20): adoption-agency-1.3"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "a": true,
-            "div": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "a",
-                    "children": [
-                      {
-                        "text": "1"
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "div",
-                    "children": [
-                      {
-                        "tag": "a",
-                        "children": [
-                          {
-                            "text": "2"
-                          }
-                        ]
-                      },
-                      {
-                        "tag": "div",
-                        "children": [
-                          {
-                            "tag": "a",
-                            "children": [
-                              {
-                                "text": "3"
-                              }
-                            ]
-                          },
-                          {
-                            "text": "4"
-                          }
-                        ]
-                      },
-                      {
-                        "text": "5"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><a>1</a><div><a>2</a><div><a>3</a>4</div>5</div></body></html>",
-        "noQuirksBodyHtml": "<a>1</a><div><a>2</a><div><a>3</a>4</div>5</div>"
-      }
-    },
-    {
-      "data": "<table><a>1<p>2</a>3</p>",
-      "errors": [
-        "(1,7): expected-doctype-but-got-start-tag",
-        "(1,10): unexpected-start-tag-implies-table-voodoo",
-        "(1,11): unexpected-character-implies-table-voodoo",
-        "(1,14): unexpected-start-tag-implies-table-voodoo",
-        "(1,15): unexpected-character-implies-table-voodoo",
-        "(1,19): unexpected-end-tag-implies-table-voodoo",
-        "(1,19): adoption-agency-1.3",
-        "(1,20): unexpected-character-implies-table-voodoo",
-        "(1,24): unexpected-end-tag-implies-table-voodoo",
-        "(1,24): eof-in-table"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "a": true,
-            "p": true,
-            "table": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "a",
-                    "children": [
-                      {
-                        "text": "1"
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "p",
-                    "children": [
-                      {
-                        "tag": "a",
-                        "children": [
-                          {
-                            "text": "2"
-                          }
-                        ]
-                      },
-                      {
-                        "text": "3"
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "table"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><a>1</a><p><a>2</a>3</p><table></table></body></html>",
-        "noQuirksBodyHtml": "<a>1</a><p><a>2</a>3</p><table></table>"
-      }
-    },
-    {
-      "data": "<b><b><a><p></a>",
-      "errors": [
-        "(1,3): expected-doctype-but-got-start-tag",
-        "(1,16): adoption-agency-1.3",
-        "(1,16): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "b": true,
-            "a": true,
-            "p": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "b",
-                    "children": [
-                      {
-                        "tag": "b",
-                        "children": [
-                          {
-                            "tag": "a"
-                          },
-                          {
-                            "tag": "p",
-                            "children": [
-                              {
-                                "tag": "a"
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><b><b><a></a><p><a></a></p></b></b></body></html>",
-        "noQuirksBodyHtml": "<b><b><a></a><p><a></a></p></b></b>"
-      }
-    },
-    {
-      "data": "<b><a><b><p></a>",
-      "errors": [
-        "(1,3): expected-doctype-but-got-start-tag",
-        "(1,16): adoption-agency-1.3",
-        "(1,16): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "b": true,
-            "a": true,
-            "p": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "b",
-                    "children": [
-                      {
-                        "tag": "a",
-                        "children": [
-                          {
-                            "tag": "b"
-                          }
-                        ]
-                      },
-                      {
-                        "tag": "b",
-                        "children": [
-                          {
-                            "tag": "p",
-                            "children": [
-                              {
-                                "tag": "a"
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><b><a><b></b></a><b><p><a></a></p></b></b></body></html>",
-        "noQuirksBodyHtml": "<b><a><b></b></a><b><p><a></a></p></b></b>"
-      }
-    },
-    {
-      "data": "<a><b><b><p></a>",
-      "errors": [
-        "(1,3): expected-doctype-but-got-start-tag",
-        "(1,16): adoption-agency-1.3",
-        "(1,16): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "a": true,
-            "b": true,
-            "p": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "a",
-                    "children": [
-                      {
-                        "tag": "b",
-                        "children": [
-                          {
-                            "tag": "b"
-                          }
-                        ]
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "b",
-                    "children": [
-                      {
-                        "tag": "b",
-                        "children": [
-                          {
-                            "tag": "p",
-                            "children": [
-                              {
-                                "tag": "a"
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><a><b><b></b></b></a><b><b><p><a></a></p></b></b></body></html>",
-        "noQuirksBodyHtml": "<a><b><b></b></b></a><b><b><p><a></a></p></b></b>"
-      }
-    },
-    {
-      "data": "<p>1<s id=\"A\">2<b id=\"B\">3</p>4</s>5</b>",
-      "errors": [
-        "(1,3): expected-doctype-but-got-start-tag",
-        "(1,30): unexpected-end-tag",
-        "(1,35): adoption-agency-1.3"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "p": true,
-            "s": true,
-            "b": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "p",
-                    "children": [
-                      {
-                        "text": "1"
-                      },
-                      {
-                        "tag": "s",
-                        "attrs": [
-                          {
-                            "name": "id",
-                            "value": "A"
-                          }
-                        ],
-                        "children": [
-                          {
-                            "text": "2"
-                          },
-                          {
-                            "tag": "b",
-                            "attrs": [
-                              {
-                                "name": "id",
-                                "value": "B"
-                              }
-                            ],
-                            "children": [
-                              {
-                                "text": "3"
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "s",
-                    "attrs": [
-                      {
-                        "name": "id",
-                        "value": "A"
-                      }
-                    ],
-                    "children": [
-                      {
-                        "tag": "b",
-                        "attrs": [
-                          {
-                            "name": "id",
-                            "value": "B"
-                          }
-                        ],
-                        "children": [
-                          {
-                            "text": "4"
-                          }
-                        ]
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "b",
-                    "attrs": [
-                      {
-                        "name": "id",
-                        "value": "B"
-                      }
-                    ],
-                    "children": [
-                      {
-                        "text": "5"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><p>1<s id=\"A\">2<b id=\"B\">3</b></s></p><s id=\"A\"><b id=\"B\">4</b></s><b id=\"B\">5</b></body></html>",
-        "noQuirksBodyHtml": "<p>1<s id=\"A\">2<b id=\"B\">3</b></s></p><s id=\"A\"><b id=\"B\">4</b></s><b id=\"B\">5</b>"
-      }
-    },
-    {
-      "data": "<table><a>1<td>2</td>3</table>",
-      "errors": [
-        "(1,7): expected-doctype-but-got-start-tag",
-        "(1,10): unexpected-start-tag-implies-table-voodoo",
-        "(1,11): unexpected-character-implies-table-voodoo",
-        "(1,15): unexpected-cell-in-table-body",
-        "(1,30): unexpected-implied-end-tag-in-table-view"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "a": true,
-            "table": true,
-            "tbody": true,
-            "tr": true,
-            "td": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "a",
-                    "children": [
-                      {
-                        "text": "1"
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "a",
-                    "children": [
-                      {
-                        "text": "3"
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "tbody",
-                        "children": [
-                          {
-                            "tag": "tr",
-                            "children": [
-                              {
-                                "tag": "td",
-                                "children": [
-                                  {
-                                    "text": "2"
-                                  }
-                                ]
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><a>1</a><a>3</a><table><tbody><tr><td>2</td></tr></tbody></table></body></html>",
-        "noQuirksBodyHtml": "<a>1</a><a>3</a><table><tbody><tr><td>2</td></tr></tbody></table>"
-      }
-    },
-    {
-      "data": "<table>A<td>B</td>C</table>",
-      "errors": [
-        "(1,7): expected-doctype-but-got-start-tag",
-        "(1,8): unexpected-character-implies-table-voodoo",
-        "(1,12): unexpected-cell-in-table-body",
-        "(1,22): unexpected-character-implies-table-voodoo"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true,
-            "tbody": true,
-            "tr": true,
-            "td": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "AC"
-                  },
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "tbody",
-                        "children": [
-                          {
-                            "tag": "tr",
-                            "children": [
-                              {
-                                "tag": "td",
-                                "children": [
-                                  {
-                                    "text": "B"
-                                  }
-                                ]
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>AC<table><tbody><tr><td>B</td></tr></tbody></table></body></html>",
-        "noQuirksBodyHtml": "AC<table><tbody><tr><td>B</td></tr></tbody></table>"
-      }
-    },
-    {
-      "data": "<a><svg><tr><input></a>",
-      "errors": [
-        "(1,3): expected-doctype-but-got-start-tag",
-        "(1,23): unexpected-end-tag",
-        "(1,23): adoption-agency-1.3"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "a": true,
-            "svg svg": true,
-            "svg tr": true,
-            "svg input": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "a",
-                    "children": [
-                      {
-                        "tag": "svg",
-                        "ns": "http://www.w3.org/2000/svg",
-                        "children": [
-                          {
-                            "tag": "tr",
-                            "ns": "http://www.w3.org/2000/svg",
-                            "children": [
-                              {
-                                "tag": "input",
-                                "ns": "http://www.w3.org/2000/svg"
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><a><svg><tr><input></input></tr></svg></a></body></html>",
-        "noQuirksBodyHtml": "<a><svg><tr><input></input></tr></svg></a>"
-      }
-    },
-    {
-      "data": "<div><a><b><div><div><div><div><div><div><div><div><div><div></a>",
-      "errors": [
-        "(1,5): expected-doctype-but-got-start-tag",
-        "(1,65): adoption-agency-1.3",
-        "(1,65): adoption-agency-1.3",
-        "(1,65): adoption-agency-1.3",
-        "(1,65): adoption-agency-1.3",
-        "(1,65): adoption-agency-1.3",
-        "(1,65): adoption-agency-1.3",
-        "(1,65): adoption-agency-1.3",
-        "(1,65): adoption-agency-1.3",
-        "(1,65): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "div": true,
-            "a": true,
-            "b": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "div",
-                    "children": [
-                      {
-                        "tag": "a",
-                        "children": [
-                          {
-                            "tag": "b"
-                          }
-                        ]
-                      },
-                      {
-                        "tag": "b",
-                        "children": [
-                          {
-                            "tag": "div",
-                            "children": [
-                              {
-                                "tag": "a"
-                              },
-                              {
-                                "tag": "div",
-                                "children": [
-                                  {
-                                    "tag": "a"
-                                  },
-                                  {
-                                    "tag": "div",
-                                    "children": [
-                                      {
-                                        "tag": "a"
-                                      },
-                                      {
-                                        "tag": "div",
-                                        "children": [
-                                          {
-                                            "tag": "a"
-                                          },
-                                          {
-                                            "tag": "div",
-                                            "children": [
-                                              {
-                                                "tag": "a"
-                                              },
-                                              {
-                                                "tag": "div",
-                                                "children": [
-                                                  {
-                                                    "tag": "a"
-                                                  },
-                                                  {
-                                                    "tag": "div",
-                                                    "children": [
-                                                      {
-                                                        "tag": "a"
-                                                      },
-                                                      {
-                                                        "tag": "div",
-                                                        "children": [
-                                                          {
-                                                            "tag": "a",
-                                                            "children": [
-                                                              {
-                                                                "tag": "div",
-                                                                "children": [
-                                                                  {
-                                                                    "tag": "div"
-                                                                  }
-                                                                ]
-                                                              }
-                                                            ]
-                                                          }
-                                                        ]
-                                                      }
-                                                    ]
-                                                  }
-                                                ]
-                                              }
-                                            ]
-                                          }
-                                        ]
-                                      }
-                                    ]
-                                  }
-                                ]
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><div><a><b></b></a><b><div><a></a><div><a></a><div><a></a><div><a></a><div><a></a><div><a></a><div><a></a><div><a><div><div></div></div></a></div></div></div></div></div></div></div></div></b></div></body></html>",
-        "noQuirksBodyHtml": "<div><a><b></b></a><b><div><a></a><div><a></a><div><a></a><div><a></a><div><a></a><div><a></a><div><a></a><div><a><div><div></div></div></a></div></div></div></div></div></div></div></div></b></div>"
-      }
-    },
-    {
-      "data": "<div><a><b><u><i><code><div></a>",
-      "errors": [
-        "(1,5): expected-doctype-but-got-start-tag",
-        "(1,32): adoption-agency-1.3",
-        "(1,32): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "div": true,
-            "a": true,
-            "b": true,
-            "u": true,
-            "i": true,
-            "code": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "div",
-                    "children": [
-                      {
-                        "tag": "a",
-                        "children": [
-                          {
-                            "tag": "b",
-                            "children": [
-                              {
-                                "tag": "u",
-                                "children": [
-                                  {
-                                    "tag": "i",
-                                    "children": [
-                                      {
-                                        "tag": "code"
-                                      }
-                                    ]
-                                  }
-                                ]
-                              }
-                            ]
-                          }
-                        ]
-                      },
-                      {
-                        "tag": "u",
-                        "children": [
-                          {
-                            "tag": "i",
-                            "children": [
-                              {
-                                "tag": "code",
-                                "children": [
-                                  {
-                                    "tag": "div",
-                                    "children": [
-                                      {
-                                        "tag": "a"
-                                      }
-                                    ]
-                                  }
-                                ]
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><div><a><b><u><i><code></code></i></u></b></a><u><i><code><div><a></a></div></code></i></u></div></body></html>",
-        "noQuirksBodyHtml": "<div><a><b><u><i><code></code></i></u></b></a><u><i><code><div><a></a></div></code></i></u></div>"
-      }
-    },
-    {
-      "data": "<b><b><b><b>x</b></b></b></b>y",
-      "errors": [
-        "(1,3): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "b": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "b",
-                    "children": [
-                      {
-                        "tag": "b",
-                        "children": [
-                          {
-                            "tag": "b",
-                            "children": [
-                              {
-                                "tag": "b",
-                                "children": [
-                                  {
-                                    "text": "x"
-                                  }
-                                ]
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  },
-                  {
-                    "text": "y"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><b><b><b><b>x</b></b></b></b>y</body></html>",
-        "noQuirksBodyHtml": "<b><b><b><b>x</b></b></b></b>y"
-      }
-    },
-    {
-      "data": "<p><b><b><b><b><p>x",
-      "errors": [
-        "(1,3): expected-doctype-but-got-start-tag",
-        "(1,18): unexpected-end-tag",
-        "(1,19): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "p": true,
-            "b": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "p",
-                    "children": [
-                      {
-                        "tag": "b",
-                        "children": [
-                          {
-                            "tag": "b",
-                            "children": [
-                              {
-                                "tag": "b",
-                                "children": [
-                                  {
-                                    "tag": "b"
-                                  }
-                                ]
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "p",
-                    "children": [
-                      {
-                        "tag": "b",
-                        "children": [
-                          {
-                            "tag": "b",
-                            "children": [
-                              {
-                                "tag": "b",
-                                "children": [
-                                  {
-                                    "text": "x"
-                                  }
-                                ]
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><p><b><b><b><b></b></b></b></b></p><p><b><b><b>x</b></b></b></p></body></html>",
-        "noQuirksBodyHtml": "<p><b><b><b><b></b></b></b></b></p><p><b><b><b>x</b></b></b></p>"
-      }
-    },
-    {
-      "data": "<b><em><foo><foob><fooc><aside></b></em>",
-      "errors": [
-        "(1,35): adoption-agency-1.3",
-        "(1,40): adoption-agency-1.3",
-        "(1,40): expected-closing-tag-but-got-eof"
-      ],
-      "fragment": {
-        "name": "div"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "b": true,
-            "em": true,
-            "foo": true,
-            "foob": true,
-            "fooc": true,
-            "aside": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "b",
-            "children": [
-              {
-                "tag": "em",
-                "children": [
-                  {
-                    "tag": "foo",
-                    "children": [
-                      {
-                        "tag": "foob",
-                        "children": [
-                          {
-                            "tag": "fooc"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          },
-          {
-            "tag": "aside",
-            "children": [
-              {
-                "tag": "b"
-              }
-            ]
-          }
-        ],
-        "html": "<b><em><foo><foob><fooc></fooc></foob></foo></em></b><aside><b></b></aside>",
-        "noQuirksBodyHtml": "<b><em><foo><foob><fooc></fooc></foob></foo></em></b><aside><b></b></aside>"
-      }
-    }
-  ],
-  "adoption02.dat": [
-    {
-      "data": "<b>1<i>2<p>3</b>4",
-      "errors": [
-        "(1,3): expected-doctype-but-got-start-tag",
-        "(1,16): adoption-agency-1.3",
-        "(1,17): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "b": true,
-            "i": true,
-            "p": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "b",
-                    "children": [
-                      {
-                        "text": "1"
-                      },
-                      {
-                        "tag": "i",
-                        "children": [
-                          {
-                            "text": "2"
-                          }
-                        ]
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "i",
-                    "children": [
-                      {
-                        "tag": "p",
-                        "children": [
-                          {
-                            "tag": "b",
-                            "children": [
-                              {
-                                "text": "3"
-                              }
-                            ]
-                          },
-                          {
-                            "text": "4"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><b>1<i>2</i></b><i><p><b>3</b>4</p></i></body></html>",
-        "noQuirksBodyHtml": "<b>1<i>2</i></b><i><p><b>3</b>4</p></i>"
-      }
-    },
-    {
-      "data": "<a><div><style></style><address><a>",
-      "errors": [
-        "(1,3): expected-doctype-but-got-start-tag",
-        "(1,35): unexpected-start-tag-implies-end-tag",
-        "(1,35): adoption-agency-1.3",
-        "(1,35): adoption-agency-1.3",
-        "(1,35): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "a": true,
-            "div": true,
-            "style": true,
-            "address": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "a"
-                  },
-                  {
-                    "tag": "div",
-                    "children": [
-                      {
-                        "tag": "a",
-                        "children": [
-                          {
-                            "tag": "style"
-                          }
-                        ]
-                      },
-                      {
-                        "tag": "address",
-                        "children": [
-                          {
-                            "tag": "a"
-                          },
-                          {
-                            "tag": "a"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><a></a><div><a><style></style></a><address><a></a><a></a></address></div></body></html>",
-        "noQuirksBodyHtml": "<a></a><div><a><style></style></a><address><a></a><a></a></address></div>"
-      }
-    }
-  ],
-  "comments01.dat": [
-    {
-      "data": "FOO<!-- BAR -->BAZ",
-      "errors": [
-        "(1,3): expected-doctype-but-got-chars"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "comment": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "FOO"
-                  },
-                  {
-                    "comment": " BAR "
-                  },
-                  {
-                    "text": "BAZ"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>FOO<!-- BAR -->BAZ</body></html>",
-        "noQuirksBodyHtml": "FOO<!-- BAR -->BAZ"
-      }
-    },
-    {
-      "data": "FOO<!-- BAR --!>BAZ",
-      "errors": [
-        "(1,3): expected-doctype-but-got-chars",
-        "(1,15): unexpected-bang-after-double-dash-in-comment"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "comment": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "FOO"
-                  },
-                  {
-                    "comment": " BAR "
-                  },
-                  {
-                    "text": "BAZ"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>FOO<!-- BAR -->BAZ</body></html>",
-        "noQuirksBodyHtml": "FOO<!-- BAR -->BAZ"
-      }
-    },
-    {
-      "data": "FOO<!-- BAR --   >BAZ",
-      "errors": [
-        "(1,3): expected-doctype-but-got-chars",
-        "(1,15): unexpected-char-in-comment",
-        "(1,21): eof-in-comment"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "comment": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "FOO"
-                  },
-                  {
-                    "comment": " BAR --   >BAZ"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>FOO<!-- BAR --   >BAZ--></body></html>",
-        "noQuirksBodyHtml": "FOO<!-- BAR --   >BAZ-->"
-      }
-    },
-    {
-      "data": "FOO<!-- BAR -- <QUX> -- MUX -->BAZ",
-      "errors": [
-        "(1,3): expected-doctype-but-got-chars",
-        "(1,15): unexpected-char-in-comment",
-        "(1,24): unexpected-char-in-comment"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "comment": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "FOO"
-                  },
-                  {
-                    "comment": " BAR -- <QUX> -- MUX "
-                  },
-                  {
-                    "text": "BAZ"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>FOO<!-- BAR -- <QUX> -- MUX -->BAZ</body></html>",
-        "noQuirksBodyHtml": "FOO<!-- BAR -- <QUX> -- MUX -->BAZ"
-      }
-    },
-    {
-      "data": "FOO<!-- BAR -- <QUX> -- MUX --!>BAZ",
-      "errors": [
-        "(1,3): expected-doctype-but-got-chars",
-        "(1,15): unexpected-char-in-comment",
-        "(1,24): unexpected-char-in-comment",
-        "(1,31): unexpected-bang-after-double-dash-in-comment"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "comment": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "FOO"
-                  },
-                  {
-                    "comment": " BAR -- <QUX> -- MUX "
-                  },
-                  {
-                    "text": "BAZ"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>FOO<!-- BAR -- <QUX> -- MUX -->BAZ</body></html>",
-        "noQuirksBodyHtml": "FOO<!-- BAR -- <QUX> -- MUX -->BAZ"
-      }
-    },
-    {
-      "data": "FOO<!-- BAR -- <QUX> -- MUX -- >BAZ",
-      "errors": [
-        "(1,3): expected-doctype-but-got-chars",
-        "(1,15): unexpected-char-in-comment",
-        "(1,24): unexpected-char-in-comment",
-        "(1,31): unexpected-char-in-comment",
-        "(1,35): eof-in-comment"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "comment": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "FOO"
-                  },
-                  {
-                    "comment": " BAR -- <QUX> -- MUX -- >BAZ"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>FOO<!-- BAR -- <QUX> -- MUX -- >BAZ--></body></html>",
-        "noQuirksBodyHtml": "FOO<!-- BAR -- <QUX> -- MUX -- >BAZ-->"
-      }
-    },
-    {
-      "data": "FOO<!---->BAZ",
-      "errors": [
-        "(1,3): expected-doctype-but-got-chars"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "comment": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "FOO"
-                  },
-                  {
-                    "comment": ""
-                  },
-                  {
-                    "text": "BAZ"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>FOO<!---->BAZ</body></html>",
-        "noQuirksBodyHtml": "FOO<!---->BAZ"
-      }
-    },
-    {
-      "data": "FOO<!--->BAZ",
-      "errors": [
-        "(1,3): expected-doctype-but-got-chars",
-        "(1,9): incorrect-comment"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "comment": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "FOO"
-                  },
-                  {
-                    "comment": ""
-                  },
-                  {
-                    "text": "BAZ"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>FOO<!---->BAZ</body></html>",
-        "noQuirksBodyHtml": "FOO<!---->BAZ"
-      }
-    },
-    {
-      "data": "FOO<!-->BAZ",
-      "errors": [
-        "(1,3): expected-doctype-but-got-chars",
-        "(1,8): incorrect-comment"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "comment": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "FOO"
-                  },
-                  {
-                    "comment": ""
-                  },
-                  {
-                    "text": "BAZ"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>FOO<!---->BAZ</body></html>",
-        "noQuirksBodyHtml": "FOO<!---->BAZ"
-      }
-    },
-    {
-      "data": "<?xml version=\"1.0\">Hi",
-      "errors": [
-        "(1,1): expected-tag-name-but-got-question-mark",
-        "(1,22): expected-doctype-but-got-chars"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "comment": true
-        },
-        "tree": [
-          {
-            "comment": "?xml version=\"1.0\""
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "Hi"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!--?xml version=\"1.0\"--><html><head></head><body>Hi</body></html>",
-        "noQuirksBodyHtml": "<!--?xml version=\"1.0\"-->Hi"
-      }
-    },
-    {
-      "data": "<?xml version=\"1.0\">",
-      "errors": [
-        "(1,1): expected-tag-name-but-got-question-mark",
-        "(1,20): expected-doctype-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "comment": true
-        },
-        "tree": [
-          {
-            "comment": "?xml version=\"1.0\""
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!--?xml version=\"1.0\"--><html><head></head><body></body></html>",
-        "noQuirksBodyHtml": "<!--?xml version=\"1.0\"-->"
-      }
-    },
-    {
-      "data": "<?xml version",
-      "errors": [
-        "(1,1): expected-tag-name-but-got-question-mark",
-        "(1,13): expected-doctype-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "comment": true
-        },
-        "tree": [
-          {
-            "comment": "?xml version"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!--?xml version--><html><head></head><body></body></html>",
-        "noQuirksBodyHtml": "<!--?xml version-->"
-      }
-    },
-    {
-      "data": "FOO<!----->BAZ",
-      "errors": [
-        "(1,3): expected-doctype-but-got-chars",
-        "(1,10): unexpected-dash-after-double-dash-in-comment"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "comment": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "FOO"
-                  },
-                  {
-                    "comment": "-"
-                  },
-                  {
-                    "text": "BAZ"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>FOO<!----->BAZ</body></html>",
-        "noQuirksBodyHtml": "FOO<!----->BAZ"
-      }
-    },
-    {
-      "data": "<html><!-- comment --><title>Comment before head</title>",
-      "errors": [
-        "(1,6): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "title": true,
-            "body": true
-          },
-          "comment": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "comment": " comment "
-              },
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "title",
-                    "children": [
-                      {
-                        "text": "Comment before head"
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><!-- comment --><head><title>Comment before head</title></head><body></body></html>",
-        "noQuirksBodyHtml": "<!-- comment --><title>Comment before head</title>"
-      }
-    }
-  ],
-  "doctype01.dat": [
-    {
-      "data": "<!DOCTYPE html>Hello",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "Hello"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body>Hello</body></html>",
-        "noQuirksBodyHtml": "Hello"
-      }
-    },
-    {
-      "data": "<!dOctYpE HtMl>Hello",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "Hello"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body>Hello</body></html>",
-        "noQuirksBodyHtml": "Hello"
-      }
-    },
-    {
-      "data": "<!DOCTYPEhtml>Hello",
-      "errors": [
-        "(1,9): need-space-after-doctype"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "Hello"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body>Hello</body></html>",
-        "noQuirksBodyHtml": "Hello"
-      }
-    },
-    {
-      "data": "<!DOCTYPE>Hello",
-      "errors": [
-        "(1,9): need-space-after-doctype",
-        "(1,10): expected-doctype-name-but-got-right-bracket",
-        "(1,10): unknown-doctype"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": ""
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "Hello"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE ><html><head></head><body>Hello</body></html>",
-        "noQuirksBodyHtml": "Hello"
-      }
-    },
-    {
-      "data": "<!DOCTYPE >Hello",
-      "errors": [
-        "(1,11): expected-doctype-name-but-got-right-bracket",
-        "(1,11): unknown-doctype"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": ""
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "Hello"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE ><html><head></head><body>Hello</body></html>",
-        "noQuirksBodyHtml": "Hello"
-      }
-    },
-    {
-      "data": "<!DOCTYPE potato>Hello",
-      "errors": [
-        "(1,17): unknown-doctype"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "potato"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "Hello"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE potato><html><head></head><body>Hello</body></html>",
-        "noQuirksBodyHtml": "Hello"
-      }
-    },
-    {
-      "data": "<!DOCTYPE potato >Hello",
-      "errors": [
-        "(1,18): unknown-doctype"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "potato"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "Hello"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE potato><html><head></head><body>Hello</body></html>",
-        "noQuirksBodyHtml": "Hello"
-      }
-    },
-    {
-      "data": "<!DOCTYPE potato taco>Hello",
-      "errors": [
-        "(1,17): expected-space-or-right-bracket-in-doctype",
-        "(1,22): unknown-doctype"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "potato"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "Hello"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE potato><html><head></head><body>Hello</body></html>",
-        "noQuirksBodyHtml": "Hello"
-      }
-    },
-    {
-      "data": "<!DOCTYPE potato taco \"ddd>Hello",
-      "errors": [
-        "(1,17): expected-space-or-right-bracket-in-doctype",
-        "(1,27): unknown-doctype"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "potato"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "Hello"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE potato><html><head></head><body>Hello</body></html>",
-        "noQuirksBodyHtml": "Hello"
-      }
-    },
-    {
-      "data": "<!DOCTYPE potato sYstEM>Hello",
-      "errors": [
-        "(1,24): unexpected-char-in-doctype",
-        "(1,24): unknown-doctype"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "potato"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "Hello"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE potato><html><head></head><body>Hello</body></html>",
-        "noQuirksBodyHtml": "Hello"
-      }
-    },
-    {
-      "data": "<!DOCTYPE potato sYstEM    >Hello",
-      "errors": [
-        "(1,28): unexpected-char-in-doctype",
-        "(1,28): unknown-doctype"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "potato"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "Hello"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE potato><html><head></head><body>Hello</body></html>",
-        "noQuirksBodyHtml": "Hello"
-      }
-    },
-    {
-      "data": "<!DOCTYPE   potato       sYstEM  ggg>Hello",
-      "errors": [
-        "(1,34): unexpected-char-in-doctype",
-        "(1,37): unknown-doctype"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "potato"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "Hello"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE potato><html><head></head><body>Hello</body></html>",
-        "noQuirksBodyHtml": "Hello"
-      }
-    },
-    {
-      "data": "<!DOCTYPE potato SYSTEM taco  >Hello",
-      "errors": [
-        "(1,25): unexpected-char-in-doctype",
-        "(1,31): unknown-doctype"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "potato"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "Hello"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE potato><html><head></head><body>Hello</body></html>",
-        "noQuirksBodyHtml": "Hello"
-      }
-    },
-    {
-      "data": "<!DOCTYPE potato SYSTEM 'taco\"'>Hello",
-      "errors": [
-        "(1,32): unknown-doctype"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "potato \"\" \"taco\"\""
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "Hello"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE potato><html><head></head><body>Hello</body></html>",
-        "noQuirksBodyHtml": "Hello"
-      }
-    },
-    {
-      "data": "<!DOCTYPE potato SYSTEM \"taco\">Hello",
-      "errors": [
-        "(1,31): unknown-doctype"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "potato \"\" \"taco\""
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "Hello"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE potato><html><head></head><body>Hello</body></html>",
-        "noQuirksBodyHtml": "Hello"
-      }
-    },
-    {
-      "data": "<!DOCTYPE potato SYSTEM \"tai'co\">Hello",
-      "errors": [
-        "(1,33): unknown-doctype"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "potato \"\" \"tai'co\""
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "Hello"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE potato><html><head></head><body>Hello</body></html>",
-        "noQuirksBodyHtml": "Hello"
-      }
-    },
-    {
-      "data": "<!DOCTYPE potato SYSTEMtaco \"ddd\">Hello",
-      "errors": [
-        "(1,24): unexpected-char-in-doctype",
-        "(1,34): unknown-doctype"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "potato"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "Hello"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE potato><html><head></head><body>Hello</body></html>",
-        "noQuirksBodyHtml": "Hello"
-      }
-    },
-    {
-      "data": "<!DOCTYPE potato grass SYSTEM taco>Hello",
-      "errors": [
-        "(1,17): expected-space-or-right-bracket-in-doctype",
-        "(1,35): unknown-doctype"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "potato"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "Hello"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE potato><html><head></head><body>Hello</body></html>",
-        "noQuirksBodyHtml": "Hello"
-      }
-    },
-    {
-      "data": "<!DOCTYPE potato pUbLIc>Hello",
-      "errors": [
-        "(1,24): unexpected-end-of-doctype",
-        "(1,24): unknown-doctype"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "potato"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "Hello"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE potato><html><head></head><body>Hello</body></html>",
-        "noQuirksBodyHtml": "Hello"
-      }
-    },
-    {
-      "data": "<!DOCTYPE potato pUbLIc >Hello",
-      "errors": [
-        "(1,25): unexpected-end-of-doctype",
-        "(1,25): unknown-doctype"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "potato"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "Hello"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE potato><html><head></head><body>Hello</body></html>",
-        "noQuirksBodyHtml": "Hello"
-      }
-    },
-    {
-      "data": "<!DOCTYPE potato pUbLIcgoof>Hello",
-      "errors": [
-        "(1,24): unexpected-char-in-doctype",
-        "(1,28): unknown-doctype"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "potato"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "Hello"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE potato><html><head></head><body>Hello</body></html>",
-        "noQuirksBodyHtml": "Hello"
-      }
-    },
-    {
-      "data": "<!DOCTYPE potato PUBLIC goof>Hello",
-      "errors": [
-        "(1,25): unexpected-char-in-doctype",
-        "(1,29): unknown-doctype"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "potato"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "Hello"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE potato><html><head></head><body>Hello</body></html>",
-        "noQuirksBodyHtml": "Hello"
-      }
-    },
-    {
-      "data": "<!DOCTYPE potato PUBLIC \"go'of\">Hello",
-      "errors": [
-        "(1,32): unknown-doctype"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "potato \"go'of\" \"\""
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "Hello"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE potato><html><head></head><body>Hello</body></html>",
-        "noQuirksBodyHtml": "Hello"
-      }
-    },
-    {
-      "data": "<!DOCTYPE potato PUBLIC 'go'of'>Hello",
-      "errors": [
-        "(1,29): unexpected-char-in-doctype",
-        "(1,32): unknown-doctype"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "potato \"go\" \"\""
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "Hello"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE potato><html><head></head><body>Hello</body></html>",
-        "noQuirksBodyHtml": "Hello"
-      }
-    },
-    {
-      "data": "<!DOCTYPE potato PUBLIC 'go:hh   of' >Hello",
-      "errors": [
-        "(1,38): unknown-doctype"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "potato \"go:hh   of\" \"\""
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "Hello"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE potato><html><head></head><body>Hello</body></html>",
-        "noQuirksBodyHtml": "Hello"
-      }
-    },
-    {
-      "data": "<!DOCTYPE potato PUBLIC \"W3C-//dfdf\" SYSTEM ggg>Hello",
-      "errors": [
-        "(1,38): unexpected-char-in-doctype",
-        "(1,48): unknown-doctype"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "potato \"W3C-//dfdf\" \"\""
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "Hello"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE potato><html><head></head><body>Hello</body></html>",
-        "noQuirksBodyHtml": "Hello"
-      }
-    },
-    {
-      "data": "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\"\n   \"http://www.w3.org/TR/html4/strict.dtd\">Hello",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd\""
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "Hello"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body>Hello</body></html>",
-        "noQuirksBodyHtml": "Hello"
-      }
-    },
-    {
-      "data": "<!DOCTYPE ...>Hello",
-      "errors": [
-        "(1,14): unknown-doctype"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "..."
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "Hello"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE ...><html><head></head><body>Hello</body></html>",
-        "noQuirksBodyHtml": "Hello"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\"\n\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">",
-      "errors": [
-        "(2,58): unknown-doctype"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\""
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body></body></html>",
-        "noQuirksBodyHtml": ""
-      }
-    },
-    {
-      "data": "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Frameset//EN\"\n\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd\">",
-      "errors": [
-        "(2,54): unknown-doctype"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html \"-//W3C//DTD XHTML 1.0 Frameset//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd\""
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body></body></html>",
-        "noQuirksBodyHtml": ""
-      }
-    },
-    {
-      "data": "<!DOCTYPE root-element [SYSTEM OR PUBLIC FPI] \"uri\" [ \n<!-- internal declarations -->\n]>",
-      "errors": [
-        "(1,23): expected-space-or-right-bracket-in-doctype",
-        "(2,30): unknown-doctype"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "doctype": true,
-          "escaped": true
-        },
-        "tree": [
-          {
-            "doctype": "root-element"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "]>",
-                    "escaped": true
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE root-element><html><head></head><body>]&gt;</body></html>",
-        "noQuirksBodyHtml": "\n]&gt;"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html PUBLIC\n  \"-//WAPFORUM//DTD XHTML Mobile 1.0//EN\"\n    \"http://www.wapforum.org/DTD/xhtml-mobile10.dtd\">",
-      "errors": [
-        "(3,53): unknown-doctype"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html \"-//WAPFORUM//DTD XHTML Mobile 1.0//EN\" \"http://www.wapforum.org/DTD/xhtml-mobile10.dtd\""
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body></body></html>",
-        "noQuirksBodyHtml": ""
-      }
-    },
-    {
-      "data": "<!DOCTYPE HTML SYSTEM \"http://www.w3.org/DTD/HTML4-strict.dtd\"><body><b>Mine!</b></body>",
-      "errors": [
-        "(1,63): unknown-doctype"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "b": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html \"\" \"http://www.w3.org/DTD/HTML4-strict.dtd\""
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "b",
-                    "children": [
-                      {
-                        "text": "Mine!"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><b>Mine!</b></body></html>",
-        "noQuirksBodyHtml": "<b>Mine!</b>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\"\"http://www.w3.org/TR/html4/strict.dtd\">",
-      "errors": [
-        "(1,50): unexpected-char-in-doctype"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd\""
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body></body></html>",
-        "noQuirksBodyHtml": ""
-      }
-    },
-    {
-      "data": "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\"'http://www.w3.org/TR/html4/strict.dtd'>",
-      "errors": [
-        "(1,50): unexpected-char-in-doctype"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd\""
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body></body></html>",
-        "noQuirksBodyHtml": ""
-      }
-    },
-    {
-      "data": "<!DOCTYPE HTML PUBLIC\"-//W3C//DTD HTML 4.01//EN\"'http://www.w3.org/TR/html4/strict.dtd'>",
-      "errors": [
-        "(1,21): unexpected-char-in-doctype",
-        "(1,49): unexpected-char-in-doctype"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd\""
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body></body></html>",
-        "noQuirksBodyHtml": ""
-      }
-    },
-    {
-      "data": "<!DOCTYPE HTML PUBLIC'-//W3C//DTD HTML 4.01//EN''http://www.w3.org/TR/html4/strict.dtd'>",
-      "errors": [
-        "(1,21): unexpected-char-in-doctype",
-        "(1,49): unexpected-char-in-doctype"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd\""
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body></body></html>",
-        "noQuirksBodyHtml": ""
-      }
-    }
-  ],
-  "domjs-unsafe.dat": [
-    {
-      "data": "<svg><![CDATA[foo\nbar]]>",
-      "errors": [
-        "(1,5): expected-doctype-but-got-start-tag",
-        "(2,6): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "svg svg": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "svg",
-                    "ns": "http://www.w3.org/2000/svg",
-                    "children": [
-                      {
-                        "text": "foo\nbar"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><svg>foo\nbar</svg></body></html>",
-        "noQuirksBodyHtml": "<svg>foo\nbar</svg>"
-      }
-    },
-    {
-      "data": "<svg><![CDATA[foo\rbar]]>",
-      "errors": [
-        "(1,5): expected-doctype-but-got-start-tag",
-        "(2,6): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "svg svg": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "svg",
-                    "ns": "http://www.w3.org/2000/svg",
-                    "children": [
-                      {
-                        "text": "foo\nbar"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><svg>foo\nbar</svg></body></html>",
-        "noQuirksBodyHtml": "<svg>foo\nbar</svg>"
-      }
-    },
-    {
-      "data": "<svg><![CDATA[foo\r\nbar]]>",
-      "errors": [
-        "(1,5): expected-doctype-but-got-start-tag",
-        "(2,6): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "svg svg": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "svg",
-                    "ns": "http://www.w3.org/2000/svg",
-                    "children": [
-                      {
-                        "text": "foo\nbar"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><svg>foo\nbar</svg></body></html>",
-        "noQuirksBodyHtml": "<svg>foo\nbar</svg>"
-      }
-    },
-    {
-      "data": "<script>a='\u0000'</script>",
-      "errors": [
-        "(1,8): expected-doctype-but-got-start-tag",
-        "(1,12): invalid-codepoint"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "a='�'",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><script>a='�'</script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script>a='�'</script>"
-      }
-    },
-    {
-      "data": "<script type=\"data\"><!--\u0000</script>",
-      "errors": [
-        "(1,20): expected-doctype-but-got-start-tag",
-        "(1,25): invalid-codepoint"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "attrs": [
-                      {
-                        "name": "type",
-                        "value": "data"
-                      }
-                    ],
-                    "children": [
-                      {
-                        "text": "<!--�",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><script type=\"data\"><!--�</script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script type=\"data\"><!--�</script>"
-      }
-    },
-    {
-      "data": "<script type=\"data\"><!--foo\u0000</script>",
-      "errors": [
-        "(1,20): expected-doctype-but-got-start-tag",
-        "(1,28): invalid-codepoint"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "attrs": [
-                      {
-                        "name": "type",
-                        "value": "data"
-                      }
-                    ],
-                    "children": [
-                      {
-                        "text": "<!--foo�",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><script type=\"data\"><!--foo�</script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script type=\"data\"><!--foo�</script>"
-      }
-    },
-    {
-      "data": "<script type=\"data\"><!-- foo-\u0000</script>",
-      "errors": [
-        "(1,20): expected-doctype-but-got-start-tag",
-        "(1,30): invalid-codepoint"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "attrs": [
-                      {
-                        "name": "type",
-                        "value": "data"
-                      }
-                    ],
-                    "children": [
-                      {
-                        "text": "<!-- foo-�",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><script type=\"data\"><!-- foo-�</script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script type=\"data\"><!-- foo-�</script>"
-      }
-    },
-    {
-      "data": "<script type=\"data\"><!-- foo--\u0000</script>",
-      "errors": [
-        "(1,20): expected-doctype-but-got-start-tag",
-        "(1,31): invalid-codepoint"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "attrs": [
-                      {
-                        "name": "type",
-                        "value": "data"
-                      }
-                    ],
-                    "children": [
-                      {
-                        "text": "<!-- foo--�",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><script type=\"data\"><!-- foo--�</script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script type=\"data\"><!-- foo--�</script>"
-      }
-    },
-    {
-      "data": "<script type=\"data\"><!-- foo-",
-      "errors": [
-        "(1,20): expected-doctype-but-got-start-tag",
-        "(1,29): expected-script-data-but-got-eof",
-        "(1,29): expected-named-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "attrs": [
-                      {
-                        "name": "type",
-                        "value": "data"
-                      }
-                    ],
-                    "children": [
-                      {
-                        "text": "<!-- foo-",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><script type=\"data\"><!-- foo-</script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script type=\"data\"><!-- foo-</script>"
-      }
-    },
-    {
-      "data": "<script type=\"data\"><!-- foo-<</script>",
-      "errors": [
-        "(1,20): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "attrs": [
-                      {
-                        "name": "type",
-                        "value": "data"
-                      }
-                    ],
-                    "children": [
-                      {
-                        "text": "<!-- foo-<",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><script type=\"data\"><!-- foo-<</script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script type=\"data\"><!-- foo-<</script>"
-      }
-    },
-    {
-      "data": "<script type=\"data\"><!-- foo-<S",
-      "errors": [
-        "(1,20): expected-doctype-but-got-start-tag",
-        "(1,31): expected-script-data-but-got-eof",
-        "(1,31): expected-named-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "attrs": [
-                      {
-                        "name": "type",
-                        "value": "data"
-                      }
-                    ],
-                    "children": [
-                      {
-                        "text": "<!-- foo-<S",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><script type=\"data\"><!-- foo-<S</script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script type=\"data\"><!-- foo-<S</script>"
-      }
-    },
-    {
-      "data": "<script type=\"data\"><!-- foo-</SCRIPT>",
-      "errors": [
-        "(1,20): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "attrs": [
-                      {
-                        "name": "type",
-                        "value": "data"
-                      }
-                    ],
-                    "children": [
-                      {
-                        "text": "<!-- foo-",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><script type=\"data\"><!-- foo-</script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script type=\"data\"><!-- foo-</script>"
-      }
-    },
-    {
-      "data": "<script type=\"data\"><!--<p></script>",
-      "errors": [
-        "(1,20): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "attrs": [
-                      {
-                        "name": "type",
-                        "value": "data"
-                      }
-                    ],
-                    "children": [
-                      {
-                        "text": "<!--<p>",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><script type=\"data\"><!--<p></script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script type=\"data\"><!--<p></script>"
-      }
-    },
-    {
-      "data": "<script type=\"data\"><!--<script></script></script>",
-      "errors": [
-        "(1,20): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "attrs": [
-                      {
-                        "name": "type",
-                        "value": "data"
-                      }
-                    ],
-                    "children": [
-                      {
-                        "text": "<!--<script></script>",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><script type=\"data\"><!--<script></script></script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script type=\"data\"><!--<script></script></script>"
-      }
-    },
-    {
-      "data": "<script type=\"data\"><!--<script>\u0000</script></script>",
-      "errors": [
-        "(1,20): expected-doctype-but-got-start-tag",
-        "(1,33): invalid-codepoint"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "attrs": [
-                      {
-                        "name": "type",
-                        "value": "data"
-                      }
-                    ],
-                    "children": [
-                      {
-                        "text": "<!--<script>�</script>",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><script type=\"data\"><!--<script>�</script></script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script type=\"data\"><!--<script>�</script></script>"
-      }
-    },
-    {
-      "data": "<script type=\"data\"><!--<script>-\u0000</script></script>",
-      "errors": [
-        "(1,20): expected-doctype-but-got-start-tag",
-        "(1,34): invalid-codepoint"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "attrs": [
-                      {
-                        "name": "type",
-                        "value": "data"
-                      }
-                    ],
-                    "children": [
-                      {
-                        "text": "<!--<script>-�</script>",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><script type=\"data\"><!--<script>-�</script></script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script type=\"data\"><!--<script>-�</script></script>"
-      }
-    },
-    {
-      "data": "<script type=\"data\"><!--<script>--\u0000</script></script>",
-      "errors": [
-        "(1,20): expected-doctype-but-got-start-tag",
-        "(1,35): invalid-codepoint"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "attrs": [
-                      {
-                        "name": "type",
-                        "value": "data"
-                      }
-                    ],
-                    "children": [
-                      {
-                        "text": "<!--<script>--�</script>",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><script type=\"data\"><!--<script>--�</script></script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script type=\"data\"><!--<script>--�</script></script>"
-      }
-    },
-    {
-      "data": "<script type=\"data\"><!--<script>---</script></script>",
-      "errors": [
-        "(1,20): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "attrs": [
-                      {
-                        "name": "type",
-                        "value": "data"
-                      }
-                    ],
-                    "children": [
-                      {
-                        "text": "<!--<script>---</script>",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><script type=\"data\"><!--<script>---</script></script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script type=\"data\"><!--<script>---</script></script>"
-      }
-    },
-    {
-      "data": "<script type=\"data\"><!--<script></scrip></SCRIPT></script>",
-      "errors": [
-        "(1,20): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "attrs": [
-                      {
-                        "name": "type",
-                        "value": "data"
-                      }
-                    ],
-                    "children": [
-                      {
-                        "text": "<!--<script></scrip></SCRIPT>",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><script type=\"data\"><!--<script></scrip></SCRIPT></script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script type=\"data\"><!--<script></scrip></SCRIPT></script>"
-      }
-    },
-    {
-      "data": "<script type=\"data\"><!--<script></scrip </SCRIPT></script>",
-      "errors": [
-        "(1,20): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "attrs": [
-                      {
-                        "name": "type",
-                        "value": "data"
-                      }
-                    ],
-                    "children": [
-                      {
-                        "text": "<!--<script></scrip </SCRIPT>",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><script type=\"data\"><!--<script></scrip </SCRIPT></script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script type=\"data\"><!--<script></scrip </SCRIPT></script>"
-      }
-    },
-    {
-      "data": "<script type=\"data\"><!--<script></scrip/</SCRIPT></script>",
-      "errors": [
-        "(1,20): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "attrs": [
-                      {
-                        "name": "type",
-                        "value": "data"
-                      }
-                    ],
-                    "children": [
-                      {
-                        "text": "<!--<script></scrip/</SCRIPT>",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><script type=\"data\"><!--<script></scrip/</SCRIPT></script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script type=\"data\"><!--<script></scrip/</SCRIPT></script>"
-      }
-    },
-    {
-      "data": "<script type=\"data\"></scrip/></script>",
-      "errors": [
-        "(1,20): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "attrs": [
-                      {
-                        "name": "type",
-                        "value": "data"
-                      }
-                    ],
-                    "children": [
-                      {
-                        "text": "</scrip/>",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><script type=\"data\"></scrip/></script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script type=\"data\"></scrip/></script>"
-      }
-    },
-    {
-      "data": "<script type=\"data\"></scrip ></script>",
-      "errors": [
-        "(1,20): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "attrs": [
-                      {
-                        "name": "type",
-                        "value": "data"
-                      }
-                    ],
-                    "children": [
-                      {
-                        "text": "</scrip >",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><script type=\"data\"></scrip ></script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script type=\"data\"></scrip ></script>"
-      }
-    },
-    {
-      "data": "<script type=\"data\"><!--</scrip></script>",
-      "errors": [
-        "(1,20): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "attrs": [
-                      {
-                        "name": "type",
-                        "value": "data"
-                      }
-                    ],
-                    "children": [
-                      {
-                        "text": "<!--</scrip>",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><script type=\"data\"><!--</scrip></script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script type=\"data\"><!--</scrip></script>"
-      }
-    },
-    {
-      "data": "<script type=\"data\"><!--</scrip </script>",
-      "errors": [
-        "(1,20): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "attrs": [
-                      {
-                        "name": "type",
-                        "value": "data"
-                      }
-                    ],
-                    "children": [
-                      {
-                        "text": "<!--</scrip ",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><script type=\"data\"><!--</scrip </script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script type=\"data\"><!--</scrip </script>"
-      }
-    },
-    {
-      "data": "<script type=\"data\"><!--</scrip/</script>",
-      "errors": [
-        "(1,20): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "attrs": [
-                      {
-                        "name": "type",
-                        "value": "data"
-                      }
-                    ],
-                    "children": [
-                      {
-                        "text": "<!--</scrip/",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><script type=\"data\"><!--</scrip/</script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script type=\"data\"><!--</scrip/</script>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><!DOCTYPE html>",
-      "errors": [
-        "(1,30): unexpected-doctype"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body></body></html>",
-        "noQuirksBodyHtml": ""
-      }
-    },
-    {
-      "data": "<html><!DOCTYPE html>",
-      "errors": [
-        "(1,6): expected-doctype-but-got-start-tag",
-        "(1,21): unexpected-doctype"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body></body></html>",
-        "noQuirksBodyHtml": ""
-      }
-    },
-    {
-      "data": "<html><head><!DOCTYPE html></head>",
-      "errors": [
-        "(1,6): expected-doctype-but-got-start-tag",
-        "(1,27): unexpected-doctype"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body></body></html>",
-        "noQuirksBodyHtml": ""
-      }
-    },
-    {
-      "data": "<html><head></head><!DOCTYPE html>",
-      "errors": [
-        "(1,6): expected-doctype-but-got-start-tag",
-        "(1,34): unexpected-doctype"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body></body></html>",
-        "noQuirksBodyHtml": ""
-      }
-    },
-    {
-      "data": "<body></body><!DOCTYPE html>",
-      "errors": [
-        "(1,6): expected-doctype-but-got-start-tag",
-        "(1,28): unexpected-doctype"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body></body></html>",
-        "noQuirksBodyHtml": ""
-      }
-    },
-    {
-      "data": "<table><!DOCTYPE html></table>",
-      "errors": [
-        "(1,7): expected-doctype-but-got-start-tag",
-        "(1,22): unexpected-doctype"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "table"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><table></table></body></html>",
-        "noQuirksBodyHtml": "<table></table>"
-      }
-    },
-    {
-      "data": "<select><!DOCTYPE html></select>",
-      "errors": [
-        "(1,8): expected-doctype-but-got-start-tag",
-        "(1,23): unexpected-doctype"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "select": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "select"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><select></select></body></html>",
-        "noQuirksBodyHtml": "<select></select>"
-      }
-    },
-    {
-      "data": "<table><colgroup><!DOCTYPE html></colgroup></table>",
-      "errors": [
-        "(1,7): expected-doctype-but-got-start-tag",
-        "(1,32): unexpected-doctype"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true,
-            "colgroup": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "colgroup"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><table><colgroup></colgroup></table></body></html>",
-        "noQuirksBodyHtml": "<table><colgroup></colgroup></table>"
-      }
-    },
-    {
-      "data": "<table><colgroup><!--test--></colgroup></table>",
-      "errors": [
-        "(1,7): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true,
-            "colgroup": true
-          },
-          "comment": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "colgroup",
-                        "children": [
-                          {
-                            "comment": "test"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><table><colgroup><!--test--></colgroup></table></body></html>",
-        "noQuirksBodyHtml": "<table><colgroup><!--test--></colgroup></table>"
-      }
-    },
-    {
-      "data": "<table><colgroup><html></colgroup></table>",
-      "errors": [
-        "(1,7): expected-doctype-but-got-start-tag",
-        "(1,23): non-html-root"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true,
-            "colgroup": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "colgroup"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><table><colgroup></colgroup></table></body></html>",
-        "noQuirksBodyHtml": "<table><colgroup></colgroup></table>"
-      }
-    },
-    {
-      "data": "<table><colgroup> foo</colgroup></table>",
-      "errors": [
-        "(1,7): expected-doctype-but-got-start-tag",
-        "(1,32): foster-parenting-character-in-table",
-        "(1,32): foster-parenting-character-in-table",
-        "(1,32): foster-parenting-character-in-table",
-        "(1,32): unexpected-end-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true,
-            "colgroup": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "foo"
-                  },
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "colgroup",
-                        "children": [
-                          {
-                            "text": " "
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>foo<table><colgroup> </colgroup></table></body></html>",
-        "noQuirksBodyHtml": "foo<table><colgroup> </colgroup></table>"
-      }
-    },
-    {
-      "data": "<select><!--test--></select>",
-      "errors": [
-        "(1,8): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "select": true
-          },
-          "comment": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "select",
-                    "children": [
-                      {
-                        "comment": "test"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><select><!--test--></select></body></html>",
-        "noQuirksBodyHtml": "<select><!--test--></select>"
-      }
-    },
-    {
-      "data": "<select><html></select>",
-      "errors": [
-        "(1,8): expected-doctype-but-got-start-tag",
-        "(1,14): non-html-root"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "select": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "select"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><select></select></body></html>",
-        "noQuirksBodyHtml": "<select></select>"
-      }
-    },
-    {
-      "data": "<frameset><html></frameset>",
-      "errors": [
-        "(1,10): expected-doctype-but-got-start-tag",
-        "(1,16): non-html-root"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "frameset": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "frameset"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><frameset></frameset></html>",
-        "noQuirksBodyHtml": ""
-      }
-    },
-    {
-      "data": "<frameset></frameset><html>",
-      "errors": [
-        "(1,10): expected-doctype-but-got-start-tag",
-        "(1,27): non-html-root"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "frameset": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "frameset"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><frameset></frameset></html>",
-        "noQuirksBodyHtml": ""
-      }
-    },
-    {
-      "data": "<frameset></frameset><!DOCTYPE html>",
-      "errors": [
-        "(1,10): expected-doctype-but-got-start-tag",
-        "(1,36): unexpected-doctype"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "frameset": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "frameset"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><frameset></frameset></html>",
-        "noQuirksBodyHtml": ""
-      }
-    },
-    {
-      "data": "<html><body></body></html><!DOCTYPE html>",
-      "errors": [
-        "(1,6): expected-doctype-but-got-start-tag",
-        "(1,41): unexpected-doctype"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body></body></html>",
-        "noQuirksBodyHtml": ""
-      }
-    },
-    {
-      "data": "<svg><!DOCTYPE html></svg>",
-      "errors": [
-        "(1,5): expected-doctype-but-got-start-tag",
-        "(1,20): unexpected-doctype"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "svg svg": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "svg",
-                    "ns": "http://www.w3.org/2000/svg"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><svg></svg></body></html>",
-        "noQuirksBodyHtml": "<svg></svg>"
-      }
-    },
-    {
-      "data": "<svg><font></font></svg>",
-      "errors": [
-        "(1,5): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "svg svg": true,
-            "svg font": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "svg",
-                    "ns": "http://www.w3.org/2000/svg",
-                    "children": [
-                      {
-                        "tag": "font",
-                        "ns": "http://www.w3.org/2000/svg"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><svg><font></font></svg></body></html>",
-        "noQuirksBodyHtml": "<svg><font></font></svg>"
-      }
-    },
-    {
-      "data": "<svg><font id=foo></font></svg>",
-      "errors": [
-        "(1,5): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "svg svg": true,
-            "svg font": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "svg",
-                    "ns": "http://www.w3.org/2000/svg",
-                    "children": [
-                      {
-                        "tag": "font",
-                        "ns": "http://www.w3.org/2000/svg",
-                        "attrs": [
-                          {
-                            "name": "id",
-                            "value": "foo"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><svg><font id=\"foo\"></font></svg></body></html>",
-        "noQuirksBodyHtml": "<svg><font id=\"foo\"></font></svg>"
-      }
-    },
-    {
-      "data": "<svg><font size=4></font></svg>",
-      "errors": [
-        "(1,5): expected-doctype-but-got-start-tag",
-        "(1,18): unexpected-html-element-in-foreign-content",
-        "(1,31): unexpected-end-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "svg svg": true,
-            "font": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "svg",
-                    "ns": "http://www.w3.org/2000/svg"
-                  },
-                  {
-                    "tag": "font",
-                    "attrs": [
-                      {
-                        "name": "size",
-                        "value": "4"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><svg></svg><font size=\"4\"></font></body></html>",
-        "noQuirksBodyHtml": "<svg><font size=\"4\"></font></svg>"
-      }
-    },
-    {
-      "data": "<svg><font color=red></font></svg>",
-      "errors": [
-        "(1,5): expected-doctype-but-got-start-tag",
-        "(1,21): unexpected-html-element-in-foreign-content",
-        "(1,34): unexpected-end-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "svg svg": true,
-            "font": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "svg",
-                    "ns": "http://www.w3.org/2000/svg"
-                  },
-                  {
-                    "tag": "font",
-                    "attrs": [
-                      {
-                        "name": "color",
-                        "value": "red"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><svg></svg><font color=\"red\"></font></body></html>",
-        "noQuirksBodyHtml": "<svg><font color=\"red\"></font></svg>"
-      }
-    },
-    {
-      "data": "<svg><font font=sans></font></svg>",
-      "errors": [
-        "(1,5): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "svg svg": true,
-            "svg font": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "svg",
-                    "ns": "http://www.w3.org/2000/svg",
-                    "children": [
-                      {
-                        "tag": "font",
-                        "ns": "http://www.w3.org/2000/svg",
-                        "attrs": [
-                          {
-                            "name": "font",
-                            "value": "sans"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><svg><font font=\"sans\"></font></svg></body></html>",
-        "noQuirksBodyHtml": "<svg><font font=\"sans\"></font></svg>"
-      }
-    }
-  ],
-  "entities01.dat": [
-    {
-      "data": "FOO&gt;BAR",
-      "errors": [
-        "(1,3): expected-doctype-but-got-chars"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "escaped": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "FOO>BAR",
-                    "escaped": true
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>FOO&gt;BAR</body></html>",
-        "noQuirksBodyHtml": "FOO&gt;BAR"
-      }
-    },
-    {
-      "data": "FOO&gtBAR",
-      "errors": [
-        "(1,3): expected-doctype-but-got-chars",
-        "(1,6): named-entity-without-semicolon"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "escaped": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "FOO>BAR",
-                    "escaped": true
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>FOO&gt;BAR</body></html>",
-        "noQuirksBodyHtml": "FOO&gt;BAR"
-      }
-    },
-    {
-      "data": "FOO&gt BAR",
-      "errors": [
-        "(1,3): expected-doctype-but-got-chars",
-        "(1,6): named-entity-without-semicolon"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "escaped": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "FOO> BAR",
-                    "escaped": true
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>FOO&gt; BAR</body></html>",
-        "noQuirksBodyHtml": "FOO&gt; BAR"
-      }
-    },
-    {
-      "data": "FOO&gt;;;BAR",
-      "errors": [
-        "(1,3): expected-doctype-but-got-chars"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "escaped": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "FOO>;;BAR",
-                    "escaped": true
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>FOO&gt;;;BAR</body></html>",
-        "noQuirksBodyHtml": "FOO&gt;;;BAR"
-      }
-    },
-    {
-      "data": "I'm &notit; I tell you",
-      "errors": [
-        "(1,4): expected-doctype-but-got-chars",
-        "(1,9): named-entity-without-semicolon"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "I'm ¬it; I tell you"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>I'm ¬it; I tell you</body></html>",
-        "noQuirksBodyHtml": "I'm ¬it; I tell you"
-      }
-    },
-    {
-      "data": "I'm &notin; I tell you",
-      "errors": [
-        "(1,4): expected-doctype-but-got-chars"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "I'm ∉ I tell you"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>I'm ∉ I tell you</body></html>",
-        "noQuirksBodyHtml": "I'm ∉ I tell you"
-      }
-    },
-    {
-      "data": "FOO& BAR",
-      "errors": [
-        "(1,3): expected-doctype-but-got-chars"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "escaped": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "FOO& BAR",
-                    "escaped": true
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>FOO&amp; BAR</body></html>",
-        "noQuirksBodyHtml": "FOO&amp; BAR"
-      }
-    },
-    {
-      "data": "FOO&<BAR>",
-      "errors": [
-        "(1,3): expected-doctype-but-got-chars",
-        "(1,9): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "bar": true
-          },
-          "escaped": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "FOO&",
-                    "escaped": true
-                  },
-                  {
-                    "tag": "bar"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>FOO&amp;<bar></bar></body></html>",
-        "noQuirksBodyHtml": "FOO&amp;<bar></bar>"
-      }
-    },
-    {
-      "data": "FOO&&&&gt;BAR",
-      "errors": [
-        "(1,3): expected-doctype-but-got-chars"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "escaped": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "FOO&&&>BAR",
-                    "escaped": true
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>FOO&amp;&amp;&amp;&gt;BAR</body></html>",
-        "noQuirksBodyHtml": "FOO&amp;&amp;&amp;&gt;BAR"
-      }
-    },
-    {
-      "data": "FOO&#41;BAR",
-      "errors": [
-        "(1,3): expected-doctype-but-got-chars"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "FOO)BAR"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>FOO)BAR</body></html>",
-        "noQuirksBodyHtml": "FOO)BAR"
-      }
-    },
-    {
-      "data": "FOO&#x41;BAR",
-      "errors": [
-        "(1,3): expected-doctype-but-got-chars"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "FOOABAR"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>FOOABAR</body></html>",
-        "noQuirksBodyHtml": "FOOABAR"
-      }
-    },
-    {
-      "data": "FOO&#X41;BAR",
-      "errors": [
-        "(1,3): expected-doctype-but-got-chars"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "FOOABAR"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>FOOABAR</body></html>",
-        "noQuirksBodyHtml": "FOOABAR"
-      }
-    },
-    {
-      "data": "FOO&#BAR",
-      "errors": [
-        "(1,3): expected-doctype-but-got-chars",
-        "(1,5): expected-numeric-entity"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "escaped": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "FOO&#BAR",
-                    "escaped": true
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>FOO&amp;#BAR</body></html>",
-        "noQuirksBodyHtml": "FOO&amp;#BAR"
-      }
-    },
-    {
-      "data": "FOO&#ZOO",
-      "errors": [
-        "(1,3): expected-doctype-but-got-chars",
-        "(1,5): expected-numeric-entity"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "escaped": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "FOO&#ZOO",
-                    "escaped": true
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>FOO&amp;#ZOO</body></html>",
-        "noQuirksBodyHtml": "FOO&amp;#ZOO"
-      }
-    },
-    {
-      "data": "FOO&#xBAR",
-      "errors": [
-        "(1,3): expected-doctype-but-got-chars",
-        "(1,7): expected-numeric-entity"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "FOOºR"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>FOOºR</body></html>",
-        "noQuirksBodyHtml": "FOOºR"
-      }
-    },
-    {
-      "data": "FOO&#xZOO",
-      "errors": [
-        "(1,3): expected-doctype-but-got-chars",
-        "(1,6): expected-numeric-entity"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "escaped": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "FOO&#xZOO",
-                    "escaped": true
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>FOO&amp;#xZOO</body></html>",
-        "noQuirksBodyHtml": "FOO&amp;#xZOO"
-      }
-    },
-    {
-      "data": "FOO&#XZOO",
-      "errors": [
-        "(1,3): expected-doctype-but-got-chars",
-        "(1,6): expected-numeric-entity"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "escaped": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "FOO&#XZOO",
-                    "escaped": true
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>FOO&amp;#XZOO</body></html>",
-        "noQuirksBodyHtml": "FOO&amp;#XZOO"
-      }
-    },
-    {
-      "data": "FOO&#41BAR",
-      "errors": [
-        "(1,3): expected-doctype-but-got-chars",
-        "(1,7): numeric-entity-without-semicolon"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "FOO)BAR"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>FOO)BAR</body></html>",
-        "noQuirksBodyHtml": "FOO)BAR"
-      }
-    },
-    {
-      "data": "FOO&#x41BAR",
-      "errors": [
-        "(1,3): expected-doctype-but-got-chars",
-        "(1,10): numeric-entity-without-semicolon"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "FOO䆺R"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>FOO䆺R</body></html>",
-        "noQuirksBodyHtml": "FOO䆺R"
-      }
-    },
-    {
-      "data": "FOO&#x41ZOO",
-      "errors": [
-        "(1,3): expected-doctype-but-got-chars",
-        "(1,8): numeric-entity-without-semicolon"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "FOOAZOO"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>FOOAZOO</body></html>",
-        "noQuirksBodyHtml": "FOOAZOO"
-      }
-    },
-    {
-      "data": "FOO&#x0000;ZOO",
-      "errors": [
-        "(1,3): expected-doctype-but-got-chars",
-        "(1,11): illegal-codepoint-for-numeric-entity"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "FOO�ZOO"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>FOO�ZOO</body></html>",
-        "noQuirksBodyHtml": "FOO�ZOO"
-      }
-    },
-    {
-      "data": "FOO&#x0078;ZOO",
-      "errors": [
-        "(1,3): expected-doctype-but-got-chars"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "FOOxZOO"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>FOOxZOO</body></html>",
-        "noQuirksBodyHtml": "FOOxZOO"
-      }
-    },
-    {
-      "data": "FOO&#x0079;ZOO",
-      "errors": [
-        "(1,3): expected-doctype-but-got-chars"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "FOOyZOO"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>FOOyZOO</body></html>",
-        "noQuirksBodyHtml": "FOOyZOO"
-      }
-    },
-    {
-      "data": "FOO&#x0080;ZOO",
-      "errors": [
-        "(1,3): expected-doctype-but-got-chars",
-        "(1,11): illegal-codepoint-for-numeric-entity"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "FOO€ZOO"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>FOO€ZOO</body></html>",
-        "noQuirksBodyHtml": "FOO€ZOO"
-      }
-    },
-    {
-      "data": "FOO&#x0081;ZOO",
-      "errors": [
-        "(1,3): expected-doctype-but-got-chars",
-        "(1,11): illegal-codepoint-for-numeric-entity"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "FOO\81ZOO"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>FOO\81ZOO</body></html>",
-        "noQuirksBodyHtml": "FOO\81ZOO"
-      }
-    },
-    {
-      "data": "FOO&#x0082;ZOO",
-      "errors": [
-        "(1,3): expected-doctype-but-got-chars",
-        "(1,11): illegal-codepoint-for-numeric-entity"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "FOO‚ZOO"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>FOO‚ZOO</body></html>",
-        "noQuirksBodyHtml": "FOO‚ZOO"
-      }
-    },
-    {
-      "data": "FOO&#x0083;ZOO",
-      "errors": [
-        "(1,3): expected-doctype-but-got-chars",
-        "(1,11): illegal-codepoint-for-numeric-entity"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "FOOƒZOO"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>FOOƒZOO</body></html>",
-        "noQuirksBodyHtml": "FOOƒZOO"
-      }
-    },
-    {
-      "data": "FOO&#x0084;ZOO",
-      "errors": [
-        "(1,3): expected-doctype-but-got-chars",
-        "(1,11): illegal-codepoint-for-numeric-entity"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "FOO„ZOO"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>FOO„ZOO</body></html>",
-        "noQuirksBodyHtml": "FOO„ZOO"
-      }
-    },
-    {
-      "data": "FOO&#x0085;ZOO",
-      "errors": [
-        "(1,3): expected-doctype-but-got-chars",
-        "(1,11): illegal-codepoint-for-numeric-entity"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "FOO…ZOO"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>FOO…ZOO</body></html>",
-        "noQuirksBodyHtml": "FOO…ZOO"
-      }
-    },
-    {
-      "data": "FOO&#x0086;ZOO",
-      "errors": [
-        "(1,3): expected-doctype-but-got-chars",
-        "(1,11): illegal-codepoint-for-numeric-entity"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "FOO†ZOO"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>FOO†ZOO</body></html>",
-        "noQuirksBodyHtml": "FOO†ZOO"
-      }
-    },
-    {
-      "data": "FOO&#x0087;ZOO",
-      "errors": [
-        "(1,3): expected-doctype-but-got-chars",
-        "(1,11): illegal-codepoint-for-numeric-entity"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "FOO‡ZOO"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>FOO‡ZOO</body></html>",
-        "noQuirksBodyHtml": "FOO‡ZOO"
-      }
-    },
-    {
-      "data": "FOO&#x0088;ZOO",
-      "errors": [
-        "(1,3): expected-doctype-but-got-chars",
-        "(1,11): illegal-codepoint-for-numeric-entity"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "FOOˆZOO"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>FOOˆZOO</body></html>",
-        "noQuirksBodyHtml": "FOOˆZOO"
-      }
-    },
-    {
-      "data": "FOO&#x0089;ZOO",
-      "errors": [
-        "(1,3): expected-doctype-but-got-chars",
-        "(1,11): illegal-codepoint-for-numeric-entity"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "FOO‰ZOO"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>FOO‰ZOO</body></html>",
-        "noQuirksBodyHtml": "FOO‰ZOO"
-      }
-    },
-    {
-      "data": "FOO&#x008A;ZOO",
-      "errors": [
-        "(1,3): expected-doctype-but-got-chars",
-        "(1,11): illegal-codepoint-for-numeric-entity"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "FOOŠZOO"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>FOOŠZOO</body></html>",
-        "noQuirksBodyHtml": "FOOŠZOO"
-      }
-    },
-    {
-      "data": "FOO&#x008B;ZOO",
-      "errors": [
-        "(1,3): expected-doctype-but-got-chars",
-        "(1,11): illegal-codepoint-for-numeric-entity"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "FOO‹ZOO"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>FOO‹ZOO</body></html>",
-        "noQuirksBodyHtml": "FOO‹ZOO"
-      }
-    },
-    {
-      "data": "FOO&#x008C;ZOO",
-      "errors": [
-        "(1,3): expected-doctype-but-got-chars",
-        "(1,11): illegal-codepoint-for-numeric-entity"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "FOOŒZOO"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>FOOŒZOO</body></html>",
-        "noQuirksBodyHtml": "FOOŒZOO"
-      }
-    },
-    {
-      "data": "FOO&#x008D;ZOO",
-      "errors": [
-        "(1,3): expected-doctype-but-got-chars",
-        "(1,11): illegal-codepoint-for-numeric-entity"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "FOO\8dZOO"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>FOO\8dZOO</body></html>",
-        "noQuirksBodyHtml": "FOO\8dZOO"
-      }
-    },
-    {
-      "data": "FOO&#x008E;ZOO",
-      "errors": [
-        "(1,3): expected-doctype-but-got-chars",
-        "(1,11): illegal-codepoint-for-numeric-entity"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "FOOŽZOO"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>FOOŽZOO</body></html>",
-        "noQuirksBodyHtml": "FOOŽZOO"
-      }
-    },
-    {
-      "data": "FOO&#x008F;ZOO",
-      "errors": [
-        "(1,3): expected-doctype-but-got-chars",
-        "(1,11): illegal-codepoint-for-numeric-entity"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "FOO\8fZOO"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>FOO\8fZOO</body></html>",
-        "noQuirksBodyHtml": "FOO\8fZOO"
-      }
-    },
-    {
-      "data": "FOO&#x0090;ZOO",
-      "errors": [
-        "(1,3): expected-doctype-but-got-chars",
-        "(1,11): illegal-codepoint-for-numeric-entity"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "FOO\90ZOO"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>FOO\90ZOO</body></html>",
-        "noQuirksBodyHtml": "FOO\90ZOO"
-      }
-    },
-    {
-      "data": "FOO&#x0091;ZOO",
-      "errors": [
-        "(1,3): expected-doctype-but-got-chars",
-        "(1,11): illegal-codepoint-for-numeric-entity"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "FOO‘ZOO"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>FOO‘ZOO</body></html>",
-        "noQuirksBodyHtml": "FOO‘ZOO"
-      }
-    },
-    {
-      "data": "FOO&#x0092;ZOO",
-      "errors": [
-        "(1,3): expected-doctype-but-got-chars",
-        "(1,11): illegal-codepoint-for-numeric-entity"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "FOO’ZOO"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>FOO’ZOO</body></html>",
-        "noQuirksBodyHtml": "FOO’ZOO"
-      }
-    },
-    {
-      "data": "FOO&#x0093;ZOO",
-      "errors": [
-        "(1,3): expected-doctype-but-got-chars",
-        "(1,11): illegal-codepoint-for-numeric-entity"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "FOO“ZOO"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>FOO“ZOO</body></html>",
-        "noQuirksBodyHtml": "FOO“ZOO"
-      }
-    },
-    {
-      "data": "FOO&#x0094;ZOO",
-      "errors": [
-        "(1,3): expected-doctype-but-got-chars",
-        "(1,11): illegal-codepoint-for-numeric-entity"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "FOO”ZOO"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>FOO”ZOO</body></html>",
-        "noQuirksBodyHtml": "FOO”ZOO"
-      }
-    },
-    {
-      "data": "FOO&#x0095;ZOO",
-      "errors": [
-        "(1,3): expected-doctype-but-got-chars",
-        "(1,11): illegal-codepoint-for-numeric-entity"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "FOO•ZOO"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>FOO•ZOO</body></html>",
-        "noQuirksBodyHtml": "FOO•ZOO"
-      }
-    },
-    {
-      "data": "FOO&#x0096;ZOO",
-      "errors": [
-        "(1,3): expected-doctype-but-got-chars",
-        "(1,11): illegal-codepoint-for-numeric-entity"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "FOO–ZOO"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>FOO–ZOO</body></html>",
-        "noQuirksBodyHtml": "FOO–ZOO"
-      }
-    },
-    {
-      "data": "FOO&#x0097;ZOO",
-      "errors": [
-        "(1,3): expected-doctype-but-got-chars",
-        "(1,11): illegal-codepoint-for-numeric-entity"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "FOO—ZOO"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>FOO—ZOO</body></html>",
-        "noQuirksBodyHtml": "FOO—ZOO"
-      }
-    },
-    {
-      "data": "FOO&#x0098;ZOO",
-      "errors": [
-        "(1,3): expected-doctype-but-got-chars",
-        "(1,11): illegal-codepoint-for-numeric-entity"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "FOO˜ZOO"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>FOO˜ZOO</body></html>",
-        "noQuirksBodyHtml": "FOO˜ZOO"
-      }
-    },
-    {
-      "data": "FOO&#x0099;ZOO",
-      "errors": [
-        "(1,3): expected-doctype-but-got-chars",
-        "(1,11): illegal-codepoint-for-numeric-entity"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "FOO™ZOO"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>FOO™ZOO</body></html>",
-        "noQuirksBodyHtml": "FOO™ZOO"
-      }
-    },
-    {
-      "data": "FOO&#x009A;ZOO",
-      "errors": [
-        "(1,3): expected-doctype-but-got-chars",
-        "(1,11): illegal-codepoint-for-numeric-entity"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "FOOšZOO"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>FOOšZOO</body></html>",
-        "noQuirksBodyHtml": "FOOšZOO"
-      }
-    },
-    {
-      "data": "FOO&#x009B;ZOO",
-      "errors": [
-        "(1,3): expected-doctype-but-got-chars",
-        "(1,11): illegal-codepoint-for-numeric-entity"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "FOO›ZOO"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>FOO›ZOO</body></html>",
-        "noQuirksBodyHtml": "FOO›ZOO"
-      }
-    },
-    {
-      "data": "FOO&#x009C;ZOO",
-      "errors": [
-        "(1,3): expected-doctype-but-got-chars",
-        "(1,11): illegal-codepoint-for-numeric-entity"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "FOOœZOO"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>FOOœZOO</body></html>",
-        "noQuirksBodyHtml": "FOOœZOO"
-      }
-    },
-    {
-      "data": "FOO&#x009D;ZOO",
-      "errors": [
-        "(1,3): expected-doctype-but-got-chars",
-        "(1,11): illegal-codepoint-for-numeric-entity"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "FOO\9dZOO"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>FOO\9dZOO</body></html>",
-        "noQuirksBodyHtml": "FOO\9dZOO"
-      }
-    },
-    {
-      "data": "FOO&#x009E;ZOO",
-      "errors": [
-        "(1,3): expected-doctype-but-got-chars",
-        "(1,11): illegal-codepoint-for-numeric-entity"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "FOOžZOO"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>FOOžZOO</body></html>",
-        "noQuirksBodyHtml": "FOOžZOO"
-      }
-    },
-    {
-      "data": "FOO&#x009F;ZOO",
-      "errors": [
-        "(1,3): expected-doctype-but-got-chars",
-        "(1,11): illegal-codepoint-for-numeric-entity"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "FOOŸZOO"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>FOOŸZOO</body></html>",
-        "noQuirksBodyHtml": "FOOŸZOO"
-      }
-    },
-    {
-      "data": "FOO&#x00A0;ZOO",
-      "errors": [
-        "(1,3): expected-doctype-but-got-chars"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "escaped": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "FOO ZOO",
-                    "escaped": true
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>FOO&nbsp;ZOO</body></html>",
-        "noQuirksBodyHtml": "FOO&nbsp;ZOO"
-      }
-    },
-    {
-      "data": "FOO&#xD7FF;ZOO",
-      "errors": [
-        "(1,3): expected-doctype-but-got-chars"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "FOO퟿ZOO"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>FOO퟿ZOO</body></html>",
-        "noQuirksBodyHtml": "FOO퟿ZOO"
-      }
-    },
-    {
-      "data": "FOO&#xD800;ZOO",
-      "errors": [
-        "(1,3): expected-doctype-but-got-chars",
-        "(1,11): illegal-codepoint-for-numeric-entity"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "FOO�ZOO"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>FOO�ZOO</body></html>",
-        "noQuirksBodyHtml": "FOO�ZOO"
-      }
-    },
-    {
-      "data": "FOO&#xD801;ZOO",
-      "errors": [
-        "(1,3): expected-doctype-but-got-chars",
-        "(1,11): illegal-codepoint-for-numeric-entity"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "FOO�ZOO"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>FOO�ZOO</body></html>",
-        "noQuirksBodyHtml": "FOO�ZOO"
-      }
-    },
-    {
-      "data": "FOO&#xDFFE;ZOO",
-      "errors": [
-        "(1,3): expected-doctype-but-got-chars",
-        "(1,11): illegal-codepoint-for-numeric-entity"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "FOO�ZOO"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>FOO�ZOO</body></html>",
-        "noQuirksBodyHtml": "FOO�ZOO"
-      }
-    },
-    {
-      "data": "FOO&#xDFFF;ZOO",
-      "errors": [
-        "(1,3): expected-doctype-but-got-chars",
-        "(1,11): illegal-codepoint-for-numeric-entity"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "FOO�ZOO"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>FOO�ZOO</body></html>",
-        "noQuirksBodyHtml": "FOO�ZOO"
-      }
-    },
-    {
-      "data": "FOO&#xE000;ZOO",
-      "errors": [
-        "(1,3): expected-doctype-but-got-chars"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "FOOZOO"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>FOOZOO</body></html>",
-        "noQuirksBodyHtml": "FOOZOO"
-      }
-    },
-    {
-      "data": "FOO&#x10FFFE;ZOO",
-      "errors": [
-        "(1,3): expected-doctype-but-got-chars",
-        "(1,13): illegal-codepoint-for-numeric-entity"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "FOO􏿾ZOO"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>FOO􏿾ZOO</body></html>",
-        "noQuirksBodyHtml": "FOO􏿾ZOO"
-      }
-    },
-    {
-      "data": "FOO&#x1087D4;ZOO",
-      "errors": [
-        "(1,3): expected-doctype-but-got-chars"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "FOO􈟔ZOO"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>FOO􈟔ZOO</body></html>",
-        "noQuirksBodyHtml": "FOO􈟔ZOO"
-      }
-    },
-    {
-      "data": "FOO&#x10FFFF;ZOO",
-      "errors": [
-        "(1,3): expected-doctype-but-got-chars",
-        "(1,13): illegal-codepoint-for-numeric-entity"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "FOO􏿿ZOO"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>FOO􏿿ZOO</body></html>",
-        "noQuirksBodyHtml": "FOO􏿿ZOO"
-      }
-    },
-    {
-      "data": "FOO&#x110000;ZOO",
-      "errors": [
-        "(1,3): expected-doctype-but-got-chars",
-        "(1,13): illegal-codepoint-for-numeric-entity"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "FOO�ZOO"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>FOO�ZOO</body></html>",
-        "noQuirksBodyHtml": "FOO�ZOO"
-      }
-    },
-    {
-      "data": "FOO&#xFFFFFF;ZOO",
-      "errors": [
-        "(1,3): expected-doctype-but-got-chars",
-        "(1,13): illegal-codepoint-for-numeric-entity"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "FOO�ZOO"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>FOO�ZOO</body></html>",
-        "noQuirksBodyHtml": "FOO�ZOO"
-      }
-    },
-    {
-      "data": "FOO&#11111111111",
-      "errors": [
-        "(1,3): expected-doctype-but-got-chars",
-        "(1,13): illegal-codepoint-for-numeric-entity",
-        "(1,13): eof-in-numeric-entity"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "FOO�"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>FOO�</body></html>",
-        "noQuirksBodyHtml": "FOO�"
-      }
-    },
-    {
-      "data": "FOO&#1111111111",
-      "errors": [
-        "(1,3): expected-doctype-but-got-chars",
-        "(1,13): illegal-codepoint-for-numeric-entity",
-        "(1,13): eof-in-numeric-entity"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "FOO�"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>FOO�</body></html>",
-        "noQuirksBodyHtml": "FOO�"
-      }
-    },
-    {
-      "data": "FOO&#111111111111",
-      "errors": [
-        "(1,3): expected-doctype-but-got-chars",
-        "(1,13): illegal-codepoint-for-numeric-entity",
-        "(1,13): eof-in-numeric-entity"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "FOO�"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>FOO�</body></html>",
-        "noQuirksBodyHtml": "FOO�"
-      }
-    },
-    {
-      "data": "FOO&#11111111111ZOO",
-      "errors": [
-        "(1,3): expected-doctype-but-got-chars",
-        "(1,16): numeric-entity-without-semicolon",
-        "(1,16): illegal-codepoint-for-numeric-entity"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "FOO�ZOO"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>FOO�ZOO</body></html>",
-        "noQuirksBodyHtml": "FOO�ZOO"
-      }
-    },
-    {
-      "data": "FOO&#1111111111ZOO",
-      "errors": [
-        "(1,3): expected-doctype-but-got-chars",
-        "(1,15): numeric-entity-without-semicolon",
-        "(1,15): illegal-codepoint-for-numeric-entity"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "FOO�ZOO"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>FOO�ZOO</body></html>",
-        "noQuirksBodyHtml": "FOO�ZOO"
-      }
-    },
-    {
-      "data": "FOO&#111111111111ZOO",
-      "errors": [
-        "(1,3): expected-doctype-but-got-chars",
-        "(1,17): numeric-entity-without-semicolon",
-        "(1,17): illegal-codepoint-for-numeric-entity"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "FOO�ZOO"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>FOO�ZOO</body></html>",
-        "noQuirksBodyHtml": "FOO�ZOO"
-      }
-    }
-  ],
-  "entities02.dat": [
-    {
-      "data": "<div bar=\"ZZ&gt;YY\"></div>",
-      "errors": [
-        "(1,20): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "div": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "div",
-                    "attrs": [
-                      {
-                        "name": "bar",
-                        "value": "ZZ>YY"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><div bar=\"ZZ>YY\"></div></body></html>",
-        "noQuirksBodyHtml": "<div bar=\"ZZ>YY\"></div>"
-      }
-    },
-    {
-      "data": "<div bar=\"ZZ&\"></div>",
-      "errors": [
-        "(1,15): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "div": true
-          },
-          "escaped": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "div",
-                    "attrs": [
-                      {
-                        "name": "bar",
-                        "value": "ZZ&",
-                        "escaped": true
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><div bar=\"ZZ&amp;\"></div></body></html>",
-        "noQuirksBodyHtml": "<div bar=\"ZZ&amp;\"></div>"
-      }
-    },
-    {
-      "data": "<div bar='ZZ&'></div>",
-      "errors": [
-        "(1,15): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "div": true
-          },
-          "escaped": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "div",
-                    "attrs": [
-                      {
-                        "name": "bar",
-                        "value": "ZZ&",
-                        "escaped": true
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><div bar=\"ZZ&amp;\"></div></body></html>",
-        "noQuirksBodyHtml": "<div bar=\"ZZ&amp;\"></div>"
-      }
-    },
-    {
-      "data": "<div bar=ZZ&></div>",
-      "errors": [
-        "(1,13): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "div": true
-          },
-          "escaped": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "div",
-                    "attrs": [
-                      {
-                        "name": "bar",
-                        "value": "ZZ&",
-                        "escaped": true
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><div bar=\"ZZ&amp;\"></div></body></html>",
-        "noQuirksBodyHtml": "<div bar=\"ZZ&amp;\"></div>"
-      }
-    },
-    {
-      "data": "<div bar=\"ZZ&gt=YY\"></div>",
-      "errors": [
-        "(1,15): named-entity-without-semicolon",
-        "(1,20): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "div": true
-          },
-          "escaped": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "div",
-                    "attrs": [
-                      {
-                        "name": "bar",
-                        "value": "ZZ&gt=YY",
-                        "escaped": true
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><div bar=\"ZZ&amp;gt=YY\"></div></body></html>",
-        "noQuirksBodyHtml": "<div bar=\"ZZ&amp;gt=YY\"></div>"
-      }
-    },
-    {
-      "data": "<div bar=\"ZZ&gt0YY\"></div>",
-      "errors": [
-        "(1,20): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "div": true
-          },
-          "escaped": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "div",
-                    "attrs": [
-                      {
-                        "name": "bar",
-                        "value": "ZZ&gt0YY",
-                        "escaped": true
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><div bar=\"ZZ&amp;gt0YY\"></div></body></html>",
-        "noQuirksBodyHtml": "<div bar=\"ZZ&amp;gt0YY\"></div>"
-      }
-    },
-    {
-      "data": "<div bar=\"ZZ&gt9YY\"></div>",
-      "errors": [
-        "(1,20): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "div": true
-          },
-          "escaped": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "div",
-                    "attrs": [
-                      {
-                        "name": "bar",
-                        "value": "ZZ&gt9YY",
-                        "escaped": true
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><div bar=\"ZZ&amp;gt9YY\"></div></body></html>",
-        "noQuirksBodyHtml": "<div bar=\"ZZ&amp;gt9YY\"></div>"
-      }
-    },
-    {
-      "data": "<div bar=\"ZZ&gtaYY\"></div>",
-      "errors": [
-        "(1,20): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "div": true
-          },
-          "escaped": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "div",
-                    "attrs": [
-                      {
-                        "name": "bar",
-                        "value": "ZZ&gtaYY",
-                        "escaped": true
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><div bar=\"ZZ&amp;gtaYY\"></div></body></html>",
-        "noQuirksBodyHtml": "<div bar=\"ZZ&amp;gtaYY\"></div>"
-      }
-    },
-    {
-      "data": "<div bar=\"ZZ&gtZYY\"></div>",
-      "errors": [
-        "(1,20): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "div": true
-          },
-          "escaped": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "div",
-                    "attrs": [
-                      {
-                        "name": "bar",
-                        "value": "ZZ&gtZYY",
-                        "escaped": true
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><div bar=\"ZZ&amp;gtZYY\"></div></body></html>",
-        "noQuirksBodyHtml": "<div bar=\"ZZ&amp;gtZYY\"></div>"
-      }
-    },
-    {
-      "data": "<div bar=\"ZZ&gt YY\"></div>",
-      "errors": [
-        "(1,15): named-entity-without-semicolon",
-        "(1,20): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "div": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "div",
-                    "attrs": [
-                      {
-                        "name": "bar",
-                        "value": "ZZ> YY"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><div bar=\"ZZ> YY\"></div></body></html>",
-        "noQuirksBodyHtml": "<div bar=\"ZZ> YY\"></div>"
-      }
-    },
-    {
-      "data": "<div bar=\"ZZ&gt\"></div>",
-      "errors": [
-        "(1,15): named-entity-without-semicolon",
-        "(1,17): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "div": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "div",
-                    "attrs": [
-                      {
-                        "name": "bar",
-                        "value": "ZZ>"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><div bar=\"ZZ>\"></div></body></html>",
-        "noQuirksBodyHtml": "<div bar=\"ZZ>\"></div>"
-      }
-    },
-    {
-      "data": "<div bar='ZZ&gt'></div>",
-      "errors": [
-        "(1,15): named-entity-without-semicolon",
-        "(1,17): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "div": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "div",
-                    "attrs": [
-                      {
-                        "name": "bar",
-                        "value": "ZZ>"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><div bar=\"ZZ>\"></div></body></html>",
-        "noQuirksBodyHtml": "<div bar=\"ZZ>\"></div>"
-      }
-    },
-    {
-      "data": "<div bar=ZZ&gt></div>",
-      "errors": [
-        "(1,14): named-entity-without-semicolon",
-        "(1,15): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "div": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "div",
-                    "attrs": [
-                      {
-                        "name": "bar",
-                        "value": "ZZ>"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><div bar=\"ZZ>\"></div></body></html>",
-        "noQuirksBodyHtml": "<div bar=\"ZZ>\"></div>"
-      }
-    },
-    {
-      "data": "<div bar=\"ZZ&pound_id=23\"></div>",
-      "errors": [
-        "(1,18): named-entity-without-semicolon",
-        "(1,26): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "div": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "div",
-                    "attrs": [
-                      {
-                        "name": "bar",
-                        "value": "ZZ£_id=23"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><div bar=\"ZZ£_id=23\"></div></body></html>",
-        "noQuirksBodyHtml": "<div bar=\"ZZ£_id=23\"></div>"
-      }
-    },
-    {
-      "data": "<div bar=\"ZZ&prod_id=23\"></div>",
-      "errors": [
-        "(1,25): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "div": true
-          },
-          "escaped": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "div",
-                    "attrs": [
-                      {
-                        "name": "bar",
-                        "value": "ZZ&prod_id=23",
-                        "escaped": true
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><div bar=\"ZZ&amp;prod_id=23\"></div></body></html>",
-        "noQuirksBodyHtml": "<div bar=\"ZZ&amp;prod_id=23\"></div>"
-      }
-    },
-    {
-      "data": "<div bar=\"ZZ&pound;_id=23\"></div>",
-      "errors": [
-        "(1,27): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "div": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "div",
-                    "attrs": [
-                      {
-                        "name": "bar",
-                        "value": "ZZ£_id=23"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><div bar=\"ZZ£_id=23\"></div></body></html>",
-        "noQuirksBodyHtml": "<div bar=\"ZZ£_id=23\"></div>"
-      }
-    },
-    {
-      "data": "<div bar=\"ZZ&prod;_id=23\"></div>",
-      "errors": [
-        "(1,26): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "div": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "div",
-                    "attrs": [
-                      {
-                        "name": "bar",
-                        "value": "ZZ∏_id=23"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><div bar=\"ZZ∏_id=23\"></div></body></html>",
-        "noQuirksBodyHtml": "<div bar=\"ZZ∏_id=23\"></div>"
-      }
-    },
-    {
-      "data": "<div bar=\"ZZ&pound=23\"></div>",
-      "errors": [
-        "(1,18): named-entity-without-semicolon",
-        "(1,23): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "div": true
-          },
-          "escaped": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "div",
-                    "attrs": [
-                      {
-                        "name": "bar",
-                        "value": "ZZ&pound=23",
-                        "escaped": true
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><div bar=\"ZZ&amp;pound=23\"></div></body></html>",
-        "noQuirksBodyHtml": "<div bar=\"ZZ&amp;pound=23\"></div>"
-      }
-    },
-    {
-      "data": "<div bar=\"ZZ&prod=23\"></div>",
-      "errors": [
-        "(1,22): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "div": true
-          },
-          "escaped": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "div",
-                    "attrs": [
-                      {
-                        "name": "bar",
-                        "value": "ZZ&prod=23",
-                        "escaped": true
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><div bar=\"ZZ&amp;prod=23\"></div></body></html>",
-        "noQuirksBodyHtml": "<div bar=\"ZZ&amp;prod=23\"></div>"
-      }
-    },
-    {
-      "data": "<div>ZZ&pound_id=23</div>",
-      "errors": [
-        "(1,5): expected-doctype-but-got-start-tag",
-        "(1,13): named-entity-without-semicolon"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "div": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "div",
-                    "children": [
-                      {
-                        "text": "ZZ£_id=23"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><div>ZZ£_id=23</div></body></html>",
-        "noQuirksBodyHtml": "<div>ZZ£_id=23</div>"
-      }
-    },
-    {
-      "data": "<div>ZZ&prod_id=23</div>",
-      "errors": [
-        "(1,5): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "div": true
-          },
-          "escaped": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "div",
-                    "children": [
-                      {
-                        "text": "ZZ&prod_id=23",
-                        "escaped": true
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><div>ZZ&amp;prod_id=23</div></body></html>",
-        "noQuirksBodyHtml": "<div>ZZ&amp;prod_id=23</div>"
-      }
-    },
-    {
-      "data": "<div>ZZ&pound;_id=23</div>",
-      "errors": [
-        "(1,5): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "div": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "div",
-                    "children": [
-                      {
-                        "text": "ZZ£_id=23"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><div>ZZ£_id=23</div></body></html>",
-        "noQuirksBodyHtml": "<div>ZZ£_id=23</div>"
-      }
-    },
-    {
-      "data": "<div>ZZ&prod;_id=23</div>",
-      "errors": [
-        "(1,5): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "div": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "div",
-                    "children": [
-                      {
-                        "text": "ZZ∏_id=23"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><div>ZZ∏_id=23</div></body></html>",
-        "noQuirksBodyHtml": "<div>ZZ∏_id=23</div>"
-      }
-    },
-    {
-      "data": "<div>ZZ&pound=23</div>",
-      "errors": [
-        "(1,5): expected-doctype-but-got-start-tag",
-        "(1,13): named-entity-without-semicolon"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "div": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "div",
-                    "children": [
-                      {
-                        "text": "ZZ£=23"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><div>ZZ£=23</div></body></html>",
-        "noQuirksBodyHtml": "<div>ZZ£=23</div>"
-      }
-    },
-    {
-      "data": "<div>ZZ&prod=23</div>",
-      "errors": [
-        "(1,5): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "div": true
-          },
-          "escaped": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "div",
-                    "children": [
-                      {
-                        "text": "ZZ&prod=23",
-                        "escaped": true
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><div>ZZ&amp;prod=23</div></body></html>",
-        "noQuirksBodyHtml": "<div>ZZ&amp;prod=23</div>"
-      }
-    },
-    {
-      "data": "<div>ZZ&AElig=</div>",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "div": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "div",
-                    "children": [
-                      {
-                        "text": "ZZÆ="
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><div>ZZÆ=</div></body></html>",
-        "noQuirksBodyHtml": "<div>ZZÆ=</div>"
-      }
-    }
-  ],
-  "foreign-fragment.dat": [
-    {
-      "data": "<nobr>X",
-      "errors": [
-        "6: HTML start tag “nobr” in a foreign namespace context.",
-        "7: End of file seen and there were open elements.",
-        "6: Unclosed element “nobr”."
-      ],
-      "fragment": {
-        "name": "path",
-        "ns": "http://www.w3.org/2000/svg"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "svg nobr": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "nobr",
-            "ns": "http://www.w3.org/2000/svg",
-            "children": [
-              {
-                "text": "X"
-              }
-            ]
-          }
-        ],
-        "html": "<nobr>X</nobr>",
-        "noQuirksBodyHtml": "<nobr>X</nobr>"
-      }
-    },
-    {
-      "data": "<font color></font>X",
-      "errors": [
-        "12: HTML start tag “font” in a foreign namespace context."
-      ],
-      "fragment": {
-        "name": "path",
-        "ns": "http://www.w3.org/2000/svg"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "svg font": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "font",
-            "ns": "http://www.w3.org/2000/svg",
-            "attrs": [
-              {
-                "name": "color",
-                "value": ""
-              }
-            ]
-          },
-          {
-            "text": "X"
-          }
-        ],
-        "html": "<font color=\"\"></font>X",
-        "noQuirksBodyHtml": "<font color=\"\"></font>X"
-      }
-    },
-    {
-      "data": "<font></font>X",
-      "errors": [],
-      "fragment": {
-        "name": "path",
-        "ns": "http://www.w3.org/2000/svg"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "svg font": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "font",
-            "ns": "http://www.w3.org/2000/svg"
-          },
-          {
-            "text": "X"
-          }
-        ],
-        "html": "<font></font>X",
-        "noQuirksBodyHtml": "<font></font>X"
-      }
-    },
-    {
-      "data": "<g></path>X",
-      "errors": [
-        "10: End tag “path” did not match the name of the current open element (“g”).",
-        "11: End of file seen and there were open elements.",
-        "3: Unclosed element “g”."
-      ],
-      "fragment": {
-        "name": "path",
-        "ns": "http://www.w3.org/2000/svg"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "svg g": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "g",
-            "ns": "http://www.w3.org/2000/svg",
-            "children": [
-              {
-                "text": "X"
-              }
-            ]
-          }
-        ],
-        "html": "<g>X</g>",
-        "noQuirksBodyHtml": "<g>X</g>"
-      }
-    },
-    {
-      "data": "</path>X",
-      "errors": [
-        "5: Stray end tag “path”."
-      ],
-      "fragment": {
-        "name": "path",
-        "ns": "http://www.w3.org/2000/svg"
-      },
-      "document": {
-        "props": {
-          "tags": {}
-        },
-        "tree": [
-          {
-            "text": "X"
-          }
-        ],
-        "html": "X",
-        "noQuirksBodyHtml": "X"
-      }
-    },
-    {
-      "data": "</foreignObject>X",
-      "errors": [
-        "5: Stray end tag “foreignobject”."
-      ],
-      "fragment": {
-        "name": "foreignObject",
-        "ns": "http://www.w3.org/2000/svg"
-      },
-      "document": {
-        "props": {
-          "tags": {}
-        },
-        "tree": [
-          {
-            "text": "X"
-          }
-        ],
-        "html": "X",
-        "noQuirksBodyHtml": "X"
-      }
-    },
-    {
-      "data": "</desc>X",
-      "errors": [
-        "5: Stray end tag “desc”."
-      ],
-      "fragment": {
-        "name": "desc",
-        "ns": "http://www.w3.org/2000/svg"
-      },
-      "document": {
-        "props": {
-          "tags": {}
-        },
-        "tree": [
-          {
-            "text": "X"
-          }
-        ],
-        "html": "X",
-        "noQuirksBodyHtml": "X"
-      }
-    },
-    {
-      "data": "</title>X",
-      "errors": [
-        "5: Stray end tag “title”."
-      ],
-      "fragment": {
-        "name": "title",
-        "ns": "http://www.w3.org/2000/svg"
-      },
-      "document": {
-        "props": {
-          "tags": {}
-        },
-        "tree": [
-          {
-            "text": "X"
-          }
-        ],
-        "html": "X",
-        "noQuirksBodyHtml": "X"
-      }
-    },
-    {
-      "data": "</svg>X",
-      "errors": [
-        "5: Stray end tag “svg”."
-      ],
-      "fragment": {
-        "name": "svg",
-        "ns": "http://www.w3.org/2000/svg"
-      },
-      "document": {
-        "props": {
-          "tags": {}
-        },
-        "tree": [
-          {
-            "text": "X"
-          }
-        ],
-        "html": "X",
-        "noQuirksBodyHtml": "X"
-      }
-    },
-    {
-      "data": "</mfenced>X",
-      "errors": [
-        "5: Stray end tag “mfenced”."
-      ],
-      "fragment": {
-        "name": "mfenced",
-        "ns": "http://www.w3.org/1998/Math/MathML"
-      },
-      "document": {
-        "props": {
-          "tags": {}
-        },
-        "tree": [
-          {
-            "text": "X"
-          }
-        ],
-        "html": "X",
-        "noQuirksBodyHtml": "X"
-      }
-    },
-    {
-      "data": "</malignmark>X",
-      "errors": [
-        "5: Stray end tag “malignmark”."
-      ],
-      "fragment": {
-        "name": "malignmark",
-        "ns": "http://www.w3.org/1998/Math/MathML"
-      },
-      "document": {
-        "props": {
-          "tags": {}
-        },
-        "tree": [
-          {
-            "text": "X"
-          }
-        ],
-        "html": "X",
-        "noQuirksBodyHtml": "X"
-      }
-    },
-    {
-      "data": "</math>X",
-      "errors": [
-        "5: Stray end tag “math”."
-      ],
-      "fragment": {
-        "name": "math",
-        "ns": "http://www.w3.org/1998/Math/MathML"
-      },
-      "document": {
-        "props": {
-          "tags": {}
-        },
-        "tree": [
-          {
-            "text": "X"
-          }
-        ],
-        "html": "X",
-        "noQuirksBodyHtml": "X"
-      }
-    },
-    {
-      "data": "</annotation-xml>X",
-      "errors": [
-        "5: Stray end tag “annotation-xml”."
-      ],
-      "fragment": {
-        "name": "annotation-xml",
-        "ns": "http://www.w3.org/1998/Math/MathML"
-      },
-      "document": {
-        "props": {
-          "tags": {}
-        },
-        "tree": [
-          {
-            "text": "X"
-          }
-        ],
-        "html": "X",
-        "noQuirksBodyHtml": "X"
-      }
-    },
-    {
-      "data": "</mtext>X",
-      "errors": [
-        "5: Stray end tag “mtext”."
-      ],
-      "fragment": {
-        "name": "mtext",
-        "ns": "http://www.w3.org/1998/Math/MathML"
-      },
-      "document": {
-        "props": {
-          "tags": {}
-        },
-        "tree": [
-          {
-            "text": "X"
-          }
-        ],
-        "html": "X",
-        "noQuirksBodyHtml": "X"
-      }
-    },
-    {
-      "data": "</mi>X",
-      "errors": [
-        "5: Stray end tag “mi”."
-      ],
-      "fragment": {
-        "name": "mi",
-        "ns": "http://www.w3.org/1998/Math/MathML"
-      },
-      "document": {
-        "props": {
-          "tags": {}
-        },
-        "tree": [
-          {
-            "text": "X"
-          }
-        ],
-        "html": "X",
-        "noQuirksBodyHtml": "X"
-      }
-    },
-    {
-      "data": "</mo>X",
-      "errors": [
-        "5: Stray end tag “mo”."
-      ],
-      "fragment": {
-        "name": "mo",
-        "ns": "http://www.w3.org/1998/Math/MathML"
-      },
-      "document": {
-        "props": {
-          "tags": {}
-        },
-        "tree": [
-          {
-            "text": "X"
-          }
-        ],
-        "html": "X",
-        "noQuirksBodyHtml": "X"
-      }
-    },
-    {
-      "data": "</mn>X",
-      "errors": [
-        "5: Stray end tag “mn”."
-      ],
-      "fragment": {
-        "name": "mn",
-        "ns": "http://www.w3.org/1998/Math/MathML"
-      },
-      "document": {
-        "props": {
-          "tags": {}
-        },
-        "tree": [
-          {
-            "text": "X"
-          }
-        ],
-        "html": "X",
-        "noQuirksBodyHtml": "X"
-      }
-    },
-    {
-      "data": "</ms>X",
-      "errors": [
-        "5: Stray end tag “ms”."
-      ],
-      "fragment": {
-        "name": "ms",
-        "ns": "http://www.w3.org/1998/Math/MathML"
-      },
-      "document": {
-        "props": {
-          "tags": {}
-        },
-        "tree": [
-          {
-            "text": "X"
-          }
-        ],
-        "html": "X",
-        "noQuirksBodyHtml": "X"
-      }
-    },
-    {
-      "data": "<b></b><mglyph/><i></i><malignmark/><u></u><ms/>X",
-      "errors": [
-        "51: Self-closing syntax (“/>”) used on a non-void HTML element. Ignoring the slash and treating as a start tag.",
-        "52: End of file seen and there were open elements.",
-        "51: Unclosed element “ms”."
-      ],
-      "fragment": {
-        "name": "ms",
-        "ns": "http://www.w3.org/1998/Math/MathML"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "b": true,
-            "math mglyph": true,
-            "i": true,
-            "math malignmark": true,
-            "u": true,
-            "ms": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "b"
-          },
-          {
-            "tag": "mglyph",
-            "ns": "http://www.w3.org/1998/Math/MathML"
-          },
-          {
-            "tag": "i"
-          },
-          {
-            "tag": "malignmark",
-            "ns": "http://www.w3.org/1998/Math/MathML"
-          },
-          {
-            "tag": "u"
-          },
-          {
-            "tag": "ms",
-            "children": [
-              {
-                "text": "X"
-              }
-            ]
-          }
-        ],
-        "html": "<b></b><mglyph></mglyph><i></i><malignmark></malignmark><u></u><ms>X</ms>",
-        "noQuirksBodyHtml": "<b></b><mglyph><i></i><malignmark><u></u><ms>X</ms></malignmark></mglyph>"
-      }
-    },
-    {
-      "data": "<malignmark></malignmark>",
-      "errors": [],
-      "fragment": {
-        "name": "ms",
-        "ns": "http://www.w3.org/1998/Math/MathML"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "math malignmark": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "malignmark",
-            "ns": "http://www.w3.org/1998/Math/MathML"
-          }
-        ],
-        "html": "<malignmark></malignmark>",
-        "noQuirksBodyHtml": "<malignmark></malignmark>"
-      }
-    },
-    {
-      "data": "<div></div>",
-      "errors": [],
-      "fragment": {
-        "name": "ms",
-        "ns": "http://www.w3.org/1998/Math/MathML"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "div": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "div"
-          }
-        ],
-        "html": "<div></div>",
-        "noQuirksBodyHtml": "<div></div>"
-      }
-    },
-    {
-      "data": "<figure></figure>",
-      "errors": [],
-      "fragment": {
-        "name": "ms",
-        "ns": "http://www.w3.org/1998/Math/MathML"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "figure": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "figure"
-          }
-        ],
-        "html": "<figure></figure>",
-        "noQuirksBodyHtml": "<figure></figure>"
-      }
-    },
-    {
-      "data": "<b></b><mglyph/><i></i><malignmark/><u></u><mn/>X",
-      "errors": [
-        "51: Self-closing syntax (“/>”) used on a non-void HTML element. Ignoring the slash and treating as a start tag.",
-        "52: End of file seen and there were open elements.",
-        "51: Unclosed element “mn”."
-      ],
-      "fragment": {
-        "name": "mn",
-        "ns": "http://www.w3.org/1998/Math/MathML"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "b": true,
-            "math mglyph": true,
-            "i": true,
-            "math malignmark": true,
-            "u": true,
-            "mn": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "b"
-          },
-          {
-            "tag": "mglyph",
-            "ns": "http://www.w3.org/1998/Math/MathML"
-          },
-          {
-            "tag": "i"
-          },
-          {
-            "tag": "malignmark",
-            "ns": "http://www.w3.org/1998/Math/MathML"
-          },
-          {
-            "tag": "u"
-          },
-          {
-            "tag": "mn",
-            "children": [
-              {
-                "text": "X"
-              }
-            ]
-          }
-        ],
-        "html": "<b></b><mglyph></mglyph><i></i><malignmark></malignmark><u></u><mn>X</mn>",
-        "noQuirksBodyHtml": "<b></b><mglyph><i></i><malignmark><u></u><mn>X</mn></malignmark></mglyph>"
-      }
-    },
-    {
-      "data": "<malignmark></malignmark>",
-      "errors": [],
-      "fragment": {
-        "name": "mn",
-        "ns": "http://www.w3.org/1998/Math/MathML"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "math malignmark": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "malignmark",
-            "ns": "http://www.w3.org/1998/Math/MathML"
-          }
-        ],
-        "html": "<malignmark></malignmark>",
-        "noQuirksBodyHtml": "<malignmark></malignmark>"
-      }
-    },
-    {
-      "data": "<div></div>",
-      "errors": [],
-      "fragment": {
-        "name": "mn",
-        "ns": "http://www.w3.org/1998/Math/MathML"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "div": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "div"
-          }
-        ],
-        "html": "<div></div>",
-        "noQuirksBodyHtml": "<div></div>"
-      }
-    },
-    {
-      "data": "<figure></figure>",
-      "errors": [],
-      "fragment": {
-        "name": "mn",
-        "ns": "http://www.w3.org/1998/Math/MathML"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "figure": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "figure"
-          }
-        ],
-        "html": "<figure></figure>",
-        "noQuirksBodyHtml": "<figure></figure>"
-      }
-    },
-    {
-      "data": "<b></b><mglyph/><i></i><malignmark/><u></u><mo/>X",
-      "errors": [
-        "51: Self-closing syntax (“/>”) used on a non-void HTML element. Ignoring the slash and treating as a start tag.",
-        "52: End of file seen and there were open elements.",
-        "51: Unclosed element “mo”."
-      ],
-      "fragment": {
-        "name": "mo",
-        "ns": "http://www.w3.org/1998/Math/MathML"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "b": true,
-            "math mglyph": true,
-            "i": true,
-            "math malignmark": true,
-            "u": true,
-            "mo": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "b"
-          },
-          {
-            "tag": "mglyph",
-            "ns": "http://www.w3.org/1998/Math/MathML"
-          },
-          {
-            "tag": "i"
-          },
-          {
-            "tag": "malignmark",
-            "ns": "http://www.w3.org/1998/Math/MathML"
-          },
-          {
-            "tag": "u"
-          },
-          {
-            "tag": "mo",
-            "children": [
-              {
-                "text": "X"
-              }
-            ]
-          }
-        ],
-        "html": "<b></b><mglyph></mglyph><i></i><malignmark></malignmark><u></u><mo>X</mo>",
-        "noQuirksBodyHtml": "<b></b><mglyph><i></i><malignmark><u></u><mo>X</mo></malignmark></mglyph>"
-      }
-    },
-    {
-      "data": "<malignmark></malignmark>",
-      "errors": [],
-      "fragment": {
-        "name": "mo",
-        "ns": "http://www.w3.org/1998/Math/MathML"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "math malignmark": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "malignmark",
-            "ns": "http://www.w3.org/1998/Math/MathML"
-          }
-        ],
-        "html": "<malignmark></malignmark>",
-        "noQuirksBodyHtml": "<malignmark></malignmark>"
-      }
-    },
-    {
-      "data": "<div></div>",
-      "errors": [],
-      "fragment": {
-        "name": "mo",
-        "ns": "http://www.w3.org/1998/Math/MathML"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "div": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "div"
-          }
-        ],
-        "html": "<div></div>",
-        "noQuirksBodyHtml": "<div></div>"
-      }
-    },
-    {
-      "data": "<figure></figure>",
-      "errors": [],
-      "fragment": {
-        "name": "mo",
-        "ns": "http://www.w3.org/1998/Math/MathML"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "figure": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "figure"
-          }
-        ],
-        "html": "<figure></figure>",
-        "noQuirksBodyHtml": "<figure></figure>"
-      }
-    },
-    {
-      "data": "<b></b><mglyph/><i></i><malignmark/><u></u><mi/>X",
-      "errors": [
-        "51: Self-closing syntax (“/>”) used on a non-void HTML element. Ignoring the slash and treating as a start tag.",
-        "52: End of file seen and there were open elements.",
-        "51: Unclosed element “mi”."
-      ],
-      "fragment": {
-        "name": "mi",
-        "ns": "http://www.w3.org/1998/Math/MathML"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "b": true,
-            "math mglyph": true,
-            "i": true,
-            "math malignmark": true,
-            "u": true,
-            "mi": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "b"
-          },
-          {
-            "tag": "mglyph",
-            "ns": "http://www.w3.org/1998/Math/MathML"
-          },
-          {
-            "tag": "i"
-          },
-          {
-            "tag": "malignmark",
-            "ns": "http://www.w3.org/1998/Math/MathML"
-          },
-          {
-            "tag": "u"
-          },
-          {
-            "tag": "mi",
-            "children": [
-              {
-                "text": "X"
-              }
-            ]
-          }
-        ],
-        "html": "<b></b><mglyph></mglyph><i></i><malignmark></malignmark><u></u><mi>X</mi>",
-        "noQuirksBodyHtml": "<b></b><mglyph><i></i><malignmark><u></u><mi>X</mi></malignmark></mglyph>"
-      }
-    },
-    {
-      "data": "<malignmark></malignmark>",
-      "errors": [],
-      "fragment": {
-        "name": "mi",
-        "ns": "http://www.w3.org/1998/Math/MathML"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "math malignmark": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "malignmark",
-            "ns": "http://www.w3.org/1998/Math/MathML"
-          }
-        ],
-        "html": "<malignmark></malignmark>",
-        "noQuirksBodyHtml": "<malignmark></malignmark>"
-      }
-    },
-    {
-      "data": "<div></div>",
-      "errors": [],
-      "fragment": {
-        "name": "mi",
-        "ns": "http://www.w3.org/1998/Math/MathML"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "div": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "div"
-          }
-        ],
-        "html": "<div></div>",
-        "noQuirksBodyHtml": "<div></div>"
-      }
-    },
-    {
-      "data": "<figure></figure>",
-      "errors": [],
-      "fragment": {
-        "name": "mi",
-        "ns": "http://www.w3.org/1998/Math/MathML"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "figure": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "figure"
-          }
-        ],
-        "html": "<figure></figure>",
-        "noQuirksBodyHtml": "<figure></figure>"
-      }
-    },
-    {
-      "data": "<b></b><mglyph/><i></i><malignmark/><u></u><mtext/>X",
-      "errors": [
-        "51: Self-closing syntax (“/>”) used on a non-void HTML element. Ignoring the slash and treating as a start tag.",
-        "52: End of file seen and there were open elements.",
-        "51: Unclosed element “mtext”."
-      ],
-      "fragment": {
-        "name": "mtext",
-        "ns": "http://www.w3.org/1998/Math/MathML"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "b": true,
-            "math mglyph": true,
-            "i": true,
-            "math malignmark": true,
-            "u": true,
-            "mtext": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "b"
-          },
-          {
-            "tag": "mglyph",
-            "ns": "http://www.w3.org/1998/Math/MathML"
-          },
-          {
-            "tag": "i"
-          },
-          {
-            "tag": "malignmark",
-            "ns": "http://www.w3.org/1998/Math/MathML"
-          },
-          {
-            "tag": "u"
-          },
-          {
-            "tag": "mtext",
-            "children": [
-              {
-                "text": "X"
-              }
-            ]
-          }
-        ],
-        "html": "<b></b><mglyph></mglyph><i></i><malignmark></malignmark><u></u><mtext>X</mtext>",
-        "noQuirksBodyHtml": "<b></b><mglyph><i></i><malignmark><u></u><mtext>X</mtext></malignmark></mglyph>"
-      }
-    },
-    {
-      "data": "<malignmark></malignmark>",
-      "errors": [],
-      "fragment": {
-        "name": "mtext",
-        "ns": "http://www.w3.org/1998/Math/MathML"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "math malignmark": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "malignmark",
-            "ns": "http://www.w3.org/1998/Math/MathML"
-          }
-        ],
-        "html": "<malignmark></malignmark>",
-        "noQuirksBodyHtml": "<malignmark></malignmark>"
-      }
-    },
-    {
-      "data": "<div></div>",
-      "errors": [],
-      "fragment": {
-        "name": "mtext",
-        "ns": "http://www.w3.org/1998/Math/MathML"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "div": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "div"
-          }
-        ],
-        "html": "<div></div>",
-        "noQuirksBodyHtml": "<div></div>"
-      }
-    },
-    {
-      "data": "<figure></figure>",
-      "errors": [],
-      "fragment": {
-        "name": "mtext",
-        "ns": "http://www.w3.org/1998/Math/MathML"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "figure": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "figure"
-          }
-        ],
-        "html": "<figure></figure>",
-        "noQuirksBodyHtml": "<figure></figure>"
-      }
-    },
-    {
-      "data": "<div></div>",
-      "errors": [
-        "5: HTML start tag “div” in a foreign namespace context."
-      ],
-      "fragment": {
-        "name": "annotation-xml",
-        "ns": "http://www.w3.org/1998/Math/MathML"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "math div": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "div",
-            "ns": "http://www.w3.org/1998/Math/MathML"
-          }
-        ],
-        "html": "<div></div>",
-        "noQuirksBodyHtml": "<div></div>"
-      }
-    },
-    {
-      "data": "<figure></figure>",
-      "errors": [],
-      "fragment": {
-        "name": "annotation-xml",
-        "ns": "http://www.w3.org/1998/Math/MathML"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "math figure": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "figure",
-            "ns": "http://www.w3.org/1998/Math/MathML"
-          }
-        ],
-        "html": "<figure></figure>",
-        "noQuirksBodyHtml": "<figure></figure>"
-      }
-    },
-    {
-      "data": "<div></div>",
-      "errors": [
-        "5: HTML start tag “div” in a foreign namespace context."
-      ],
-      "fragment": {
-        "name": "math",
-        "ns": "http://www.w3.org/1998/Math/MathML"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "math div": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "div",
-            "ns": "http://www.w3.org/1998/Math/MathML"
-          }
-        ],
-        "html": "<div></div>",
-        "noQuirksBodyHtml": "<div></div>"
-      }
-    },
-    {
-      "data": "<figure></figure>",
-      "errors": [],
-      "fragment": {
-        "name": "math",
-        "ns": "http://www.w3.org/1998/Math/MathML"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "math figure": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "figure",
-            "ns": "http://www.w3.org/1998/Math/MathML"
-          }
-        ],
-        "html": "<figure></figure>",
-        "noQuirksBodyHtml": "<figure></figure>"
-      }
-    },
-    {
-      "data": "<div></div>",
-      "errors": [],
-      "fragment": {
-        "name": "foreignObject",
-        "ns": "http://www.w3.org/2000/svg"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "div": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "div"
-          }
-        ],
-        "html": "<div></div>",
-        "noQuirksBodyHtml": "<div></div>"
-      }
-    },
-    {
-      "data": "<figure></figure>",
-      "errors": [],
-      "fragment": {
-        "name": "foreignObject",
-        "ns": "http://www.w3.org/2000/svg"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "figure": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "figure"
-          }
-        ],
-        "html": "<figure></figure>",
-        "noQuirksBodyHtml": "<figure></figure>"
-      }
-    },
-    {
-      "data": "<div></div>",
-      "errors": [],
-      "fragment": {
-        "name": "title",
-        "ns": "http://www.w3.org/2000/svg"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "div": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "div"
-          }
-        ],
-        "html": "<div></div>",
-        "noQuirksBodyHtml": "<div></div>"
-      }
-    },
-    {
-      "data": "<figure></figure>",
-      "errors": [],
-      "fragment": {
-        "name": "title",
-        "ns": "http://www.w3.org/2000/svg"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "figure": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "figure"
-          }
-        ],
-        "html": "<figure></figure>",
-        "noQuirksBodyHtml": "<figure></figure>"
-      }
-    },
-    {
-      "data": "<figure></figure>",
-      "errors": [],
-      "fragment": {
-        "name": "desc",
-        "ns": "http://www.w3.org/2000/svg"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "figure": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "figure"
-          }
-        ],
-        "html": "<figure></figure>",
-        "noQuirksBodyHtml": "<figure></figure>"
-      }
-    },
-    {
-      "data": "<div><h1>X</h1></div>",
-      "errors": [
-        "5: HTML start tag “div” in a foreign namespace context.",
-        "9: HTML start tag “h1” in a foreign namespace context."
-      ],
-      "fragment": {
-        "name": "svg",
-        "ns": "http://www.w3.org/2000/svg"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "svg div": true,
-            "svg h1": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "div",
-            "ns": "http://www.w3.org/2000/svg",
-            "children": [
-              {
-                "tag": "h1",
-                "ns": "http://www.w3.org/2000/svg",
-                "children": [
-                  {
-                    "text": "X"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<div><h1>X</h1></div>",
-        "noQuirksBodyHtml": "<div><h1>X</h1></div>"
-      }
-    },
-    {
-      "data": "<div></div>",
-      "errors": [
-        "5: HTML start tag “div” in a foreign namespace context."
-      ],
-      "fragment": {
-        "name": "svg",
-        "ns": "http://www.w3.org/2000/svg"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "svg div": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "div",
-            "ns": "http://www.w3.org/2000/svg"
-          }
-        ],
-        "html": "<div></div>",
-        "noQuirksBodyHtml": "<div></div>"
-      }
-    },
-    {
-      "data": "<div></div>",
-      "errors": [],
-      "fragment": {
-        "name": "desc",
-        "ns": "http://www.w3.org/2000/svg"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "div": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "div"
-          }
-        ],
-        "html": "<div></div>",
-        "noQuirksBodyHtml": "<div></div>"
-      }
-    },
-    {
-      "data": "<figure></figure>",
-      "errors": [],
-      "fragment": {
-        "name": "desc",
-        "ns": "http://www.w3.org/2000/svg"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "figure": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "figure"
-          }
-        ],
-        "html": "<figure></figure>",
-        "noQuirksBodyHtml": "<figure></figure>"
-      }
-    },
-    {
-      "data": "<plaintext><foo>",
-      "errors": [
-        "(1,16): expected-closing-tag-but-got-eof"
-      ],
-      "fragment": {
-        "name": "desc",
-        "ns": "http://www.w3.org/2000/svg"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "plaintext": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "plaintext",
-            "children": [
-              {
-                "text": "<foo>",
-                "no_escape": true
-              }
-            ]
-          }
-        ],
-        "html": "<plaintext><foo></plaintext>",
-        "noQuirksBodyHtml": "<plaintext><foo></plaintext>"
-      }
-    },
-    {
-      "data": "<frameset>X",
-      "errors": [
-        "6: Stray start tag “frameset”."
-      ],
-      "fragment": {
-        "name": "desc",
-        "ns": "http://www.w3.org/2000/svg"
-      },
-      "document": {
-        "props": {
-          "tags": {}
-        },
-        "tree": [
-          {
-            "text": "X"
-          }
-        ],
-        "html": "X",
-        "noQuirksBodyHtml": "X"
-      }
-    },
-    {
-      "data": "<head>X",
-      "errors": [
-        "6: Stray start tag “head”."
-      ],
-      "fragment": {
-        "name": "desc",
-        "ns": "http://www.w3.org/2000/svg"
-      },
-      "document": {
-        "props": {
-          "tags": {}
-        },
-        "tree": [
-          {
-            "text": "X"
-          }
-        ],
-        "html": "X",
-        "noQuirksBodyHtml": "X"
-      }
-    },
-    {
-      "data": "<body>X",
-      "errors": [
-        "6: Stray start tag “body”."
-      ],
-      "fragment": {
-        "name": "desc",
-        "ns": "http://www.w3.org/2000/svg"
-      },
-      "document": {
-        "props": {
-          "tags": {}
-        },
-        "tree": [
-          {
-            "text": "X"
-          }
-        ],
-        "html": "X",
-        "noQuirksBodyHtml": "X"
-      }
-    },
-    {
-      "data": "<html>X",
-      "errors": [
-        "6: Stray start tag “html”."
-      ],
-      "fragment": {
-        "name": "desc",
-        "ns": "http://www.w3.org/2000/svg"
-      },
-      "document": {
-        "props": {
-          "tags": {}
-        },
-        "tree": [
-          {
-            "text": "X"
-          }
-        ],
-        "html": "X",
-        "noQuirksBodyHtml": "X"
-      }
-    },
-    {
-      "data": "<html class=\"foo\">X",
-      "errors": [
-        "6: Stray start tag “html”."
-      ],
-      "fragment": {
-        "name": "desc",
-        "ns": "http://www.w3.org/2000/svg"
-      },
-      "document": {
-        "props": {
-          "tags": {}
-        },
-        "tree": [
-          {
-            "text": "X"
-          }
-        ],
-        "html": "X",
-        "noQuirksBodyHtml": "X"
-      }
-    },
-    {
-      "data": "<body class=\"foo\">X",
-      "errors": [
-        "6: Stray start tag “body”."
-      ],
-      "fragment": {
-        "name": "desc",
-        "ns": "http://www.w3.org/2000/svg"
-      },
-      "document": {
-        "props": {
-          "tags": {}
-        },
-        "tree": [
-          {
-            "text": "X"
-          }
-        ],
-        "html": "X",
-        "noQuirksBodyHtml": "X"
-      }
-    }
-  ],
-  "html5test-com.dat": [
-    {
-      "data": "<div<div>",
-      "errors": [
-        "(1,9): expected-doctype-but-got-start-tag",
-        "(1,9): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "div<div": true
-          },
-          "tagWithLt": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "div<div"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><div<div></div<div></body></html>",
-        "noQuirksBodyHtml": "<div<div></div<div>"
-      }
-    },
-    {
-      "data": "<div foo<bar=''>",
-      "errors": [
-        "(1,9): invalid-character-in-attribute-name",
-        "(1,16): expected-doctype-but-got-start-tag",
-        "(1,16): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "div": true
-          },
-          "attrWithFunnyChar": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "div",
-                    "attrs": [
-                      {
-                        "name": "foo<bar",
-                        "value": ""
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><div foo<bar=\"\"></div></body></html>",
-        "noQuirksBodyHtml": "<div foo<bar=\"\"></div>"
-      }
-    },
-    {
-      "data": "<div foo=`bar`>",
-      "errors": [
-        "(1,10): equals-in-unquoted-attribute-value",
-        "(1,14): unexpected-character-in-unquoted-attribute-value",
-        "(1,15): expected-doctype-but-got-start-tag",
-        "(1,15): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "div": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "div",
-                    "attrs": [
-                      {
-                        "name": "foo",
-                        "value": "`bar`"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><div foo=\"`bar`\"></div></body></html>",
-        "noQuirksBodyHtml": "<div foo=\"`bar`\"></div>"
-      }
-    },
-    {
-      "data": "<div \\\"foo=''>",
-      "errors": [
-        "(1,7): invalid-character-in-attribute-name",
-        "(1,14): expected-doctype-but-got-start-tag",
-        "(1,14): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "div": true
-          },
-          "attrWithFunnyChar": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "div",
-                    "attrs": [
-                      {
-                        "name": "\\\"foo",
-                        "value": ""
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><div \\\"foo=\"\"></div></body></html>",
-        "noQuirksBodyHtml": "<div \\\"foo=\"\"></div>"
-      }
-    },
-    {
-      "data": "<a href='\\nbar'></a>",
-      "errors": [
-        "(1,16): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "a": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "a",
-                    "attrs": [
-                      {
-                        "name": "href",
-                        "value": "\\nbar"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><a href=\"\\nbar\"></a></body></html>",
-        "noQuirksBodyHtml": "<a href=\"\\nbar\"></a>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html>",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body></body></html>",
-        "noQuirksBodyHtml": ""
-      }
-    },
-    {
-      "data": "&lang;&rang;",
-      "errors": [
-        "(1,6): expected-doctype-but-got-chars"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "⟨⟩"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>⟨⟩</body></html>",
-        "noQuirksBodyHtml": "⟨⟩"
-      }
-    },
-    {
-      "data": "&apos;",
-      "errors": [
-        "(1,6): expected-doctype-but-got-chars"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "'"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>'</body></html>",
-        "noQuirksBodyHtml": "'"
-      }
-    },
-    {
-      "data": "&ImaginaryI;",
-      "errors": [
-        "(1,12): expected-doctype-but-got-chars"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "ⅈ"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>ⅈ</body></html>",
-        "noQuirksBodyHtml": "ⅈ"
-      }
-    },
-    {
-      "data": "&Kopf;",
-      "errors": [
-        "(1,6): expected-doctype-but-got-chars"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "𝕂"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>𝕂</body></html>",
-        "noQuirksBodyHtml": "𝕂"
-      }
-    },
-    {
-      "data": "&notinva;",
-      "errors": [
-        "(1,9): expected-doctype-but-got-chars"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "∉"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>∉</body></html>",
-        "noQuirksBodyHtml": "∉"
-      }
-    },
-    {
-      "data": "<?import namespace=\"foo\" implementation=\"#bar\">",
-      "errors": [
-        "(1,1): expected-tag-name-but-got-question-mark",
-        "(1,47): expected-doctype-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "comment": true
-        },
-        "tree": [
-          {
-            "comment": "?import namespace=\"foo\" implementation=\"#bar\""
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!--?import namespace=\"foo\" implementation=\"#bar\"--><html><head></head><body></body></html>",
-        "noQuirksBodyHtml": "<!--?import namespace=\"foo\" implementation=\"#bar\"-->"
-      }
-    },
-    {
-      "data": "<!--foo--bar-->",
-      "errors": [
-        "(1,10): unexpected-char-in-comment",
-        "(1,15): expected-doctype-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "comment": true
-        },
-        "tree": [
-          {
-            "comment": "foo--bar"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!--foo--bar--><html><head></head><body></body></html>",
-        "noQuirksBodyHtml": "<!--foo--bar-->"
-      }
-    },
-    {
-      "data": "<![CDATA[x]]>",
-      "errors": [
-        "(1,2): expected-dashes-or-doctype",
-        "(1,13): expected-doctype-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "comment": true
-        },
-        "tree": [
-          {
-            "comment": "[CDATA[x]]"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!--[CDATA[x]]--><html><head></head><body></body></html>",
-        "noQuirksBodyHtml": "<!--[CDATA[x]]-->"
-      }
-    },
-    {
-      "data": "<textarea><!--</textarea>--></textarea>",
-      "errors": [
-        "(1,10): expected-doctype-but-got-start-tag",
-        "(1,39): unexpected-end-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "textarea": true
-          },
-          "escaped": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "textarea",
-                    "children": [
-                      {
-                        "text": "<!--",
-                        "escaped": true
-                      }
-                    ]
-                  },
-                  {
-                    "text": "-->",
-                    "escaped": true
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><textarea>&lt;!--</textarea>--&gt;</body></html>",
-        "noQuirksBodyHtml": "<textarea>&lt;!--</textarea>--&gt;"
-      }
-    },
-    {
-      "data": "<textarea><!--</textarea>-->",
-      "errors": [
-        "(1,10): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "textarea": true
-          },
-          "escaped": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "textarea",
-                    "children": [
-                      {
-                        "text": "<!--",
-                        "escaped": true
-                      }
-                    ]
-                  },
-                  {
-                    "text": "-->",
-                    "escaped": true
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><textarea>&lt;!--</textarea>--&gt;</body></html>",
-        "noQuirksBodyHtml": "<textarea>&lt;!--</textarea>--&gt;"
-      }
-    },
-    {
-      "data": "<style><!--</style>--></style>",
-      "errors": [
-        "(1,7): expected-doctype-but-got-start-tag",
-        "(1,30): unexpected-end-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "style": true,
-            "body": true
-          },
-          "no_escape": true,
-          "escaped": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "style",
-                    "children": [
-                      {
-                        "text": "<!--",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "-->",
-                    "escaped": true
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><style><!--</style></head><body>--&gt;</body></html>",
-        "noQuirksBodyHtml": "<style><!--</style>--&gt;"
-      }
-    },
-    {
-      "data": "<style><!--</style>-->",
-      "errors": [
-        "(1,7): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "style": true,
-            "body": true
-          },
-          "no_escape": true,
-          "escaped": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "style",
-                    "children": [
-                      {
-                        "text": "<!--",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "-->",
-                    "escaped": true
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><style><!--</style></head><body>--&gt;</body></html>",
-        "noQuirksBodyHtml": "<style><!--</style>--&gt;"
-      }
-    },
-    {
-      "data": "<ul><li>A </li> <li>B</li></ul>",
-      "errors": [
-        "(1,4): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "ul": true,
-            "li": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "ul",
-                    "children": [
-                      {
-                        "tag": "li",
-                        "children": [
-                          {
-                            "text": "A "
-                          }
-                        ]
-                      },
-                      {
-                        "text": " "
-                      },
-                      {
-                        "tag": "li",
-                        "children": [
-                          {
-                            "text": "B"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><ul><li>A </li> <li>B</li></ul></body></html>",
-        "noQuirksBodyHtml": "<ul><li>A </li> <li>B</li></ul>"
-      }
-    },
-    {
-      "data": "<table><form><input type=hidden><input></form><div></div></table>",
-      "errors": [
-        "(1,7): expected-doctype-but-got-start-tag",
-        "(1,13): unexpected-form-in-table",
-        "(1,32): unexpected-hidden-input-in-table",
-        "(1,39): unexpected-start-tag-implies-table-voodoo",
-        "(1,46): unexpected-end-tag-implies-table-voodoo",
-        "(1,46): unexpected-end-tag",
-        "(1,51): unexpected-start-tag-implies-table-voodoo",
-        "(1,57): unexpected-end-tag-implies-table-voodoo"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "input": true,
-            "div": true,
-            "table": true,
-            "form": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "input"
-                  },
-                  {
-                    "tag": "div"
-                  },
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "form"
-                      },
-                      {
-                        "tag": "input",
-                        "attrs": [
-                          {
-                            "name": "type",
-                            "value": "hidden"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><input><div></div><table><form></form><input type=\"hidden\"></table></body></html>",
-        "noQuirksBodyHtml": "<input><div></div><table><form></form><input type=\"hidden\"></table>"
-      }
-    },
-    {
-      "data": "<i>A<b>B<p></i>C</b>D",
-      "errors": [
-        "(1,3): expected-doctype-but-got-start-tag",
-        "(1,15): adoption-agency-1.3",
-        "(1,20): adoption-agency-1.3"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "i": true,
-            "b": true,
-            "p": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "i",
-                    "children": [
-                      {
-                        "text": "A"
-                      },
-                      {
-                        "tag": "b",
-                        "children": [
-                          {
-                            "text": "B"
-                          }
-                        ]
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "b"
-                  },
-                  {
-                    "tag": "p",
-                    "children": [
-                      {
-                        "tag": "b",
-                        "children": [
-                          {
-                            "tag": "i"
-                          },
-                          {
-                            "text": "C"
-                          }
-                        ]
-                      },
-                      {
-                        "text": "D"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><i>A<b>B</b></i><b></b><p><b><i></i>C</b>D</p></body></html>",
-        "noQuirksBodyHtml": "<i>A<b>B</b></i><b></b><p><b><i></i>C</b>D</p>"
-      }
-    },
-    {
-      "data": "<div></div>",
-      "errors": [
-        "(1,5): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "div": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "div"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><div></div></body></html>",
-        "noQuirksBodyHtml": "<div></div>"
-      }
-    },
-    {
-      "data": "<svg></svg>",
-      "errors": [
-        "(1,5): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "svg svg": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "svg",
-                    "ns": "http://www.w3.org/2000/svg"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><svg></svg></body></html>",
-        "noQuirksBodyHtml": "<svg></svg>"
-      }
-    },
-    {
-      "data": "<math></math>",
-      "errors": [
-        "(1,6): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "math math": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "math",
-                    "ns": "http://www.w3.org/1998/Math/MathML"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><math></math></body></html>",
-        "noQuirksBodyHtml": "<math></math>"
-      }
-    }
-  ],
-  "inbody01.dat": [
-    {
-      "data": "<button>1</foo>",
-      "errors": [
-        "(1,8): expected-doctype-but-got-start-tag",
-        "(1,15): unexpected-end-tag",
-        "(1,15): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "button": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "button",
-                    "children": [
-                      {
-                        "text": "1"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><button>1</button></body></html>",
-        "noQuirksBodyHtml": "<button>1</button>"
-      }
-    },
-    {
-      "data": "<foo>1<p>2</foo>",
-      "errors": [
-        "(1,5): expected-doctype-but-got-start-tag",
-        "(1,16): unexpected-end-tag",
-        "(1,16): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "foo": true,
-            "p": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "foo",
-                    "children": [
-                      {
-                        "text": "1"
-                      },
-                      {
-                        "tag": "p",
-                        "children": [
-                          {
-                            "text": "2"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><foo>1<p>2</p></foo></body></html>",
-        "noQuirksBodyHtml": "<foo>1<p>2</p></foo>"
-      }
-    },
-    {
-      "data": "<dd>1</foo>",
-      "errors": [
-        "(1,4): expected-doctype-but-got-start-tag",
-        "(1,11): unexpected-end-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "dd": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "dd",
-                    "children": [
-                      {
-                        "text": "1"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><dd>1</dd></body></html>",
-        "noQuirksBodyHtml": "<dd>1</dd>"
-      }
-    },
-    {
-      "data": "<foo>1<dd>2</foo>",
-      "errors": [
-        "(1,5): expected-doctype-but-got-start-tag",
-        "(1,17): unexpected-end-tag",
-        "(1,17): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "foo": true,
-            "dd": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "foo",
-                    "children": [
-                      {
-                        "text": "1"
-                      },
-                      {
-                        "tag": "dd",
-                        "children": [
-                          {
-                            "text": "2"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><foo>1<dd>2</dd></foo></body></html>",
-        "noQuirksBodyHtml": "<foo>1<dd>2</dd></foo>"
-      }
-    }
-  ],
-  "isindex.dat": [
-    {
-      "data": "<isindex>",
-      "errors": [
-        "(1,9): expected-doctype-but-got-start-tag",
-        "(1,9): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "isindex": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "isindex"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><isindex></isindex></body></html>",
-        "noQuirksBodyHtml": "<isindex></isindex>"
-      }
-    },
-    {
-      "data": "<isindex name=\"A\" action=\"B\" prompt=\"C\" foo=\"D\">",
-      "errors": [
-        "(1,48): expected-doctype-but-got-start-tag",
-        "(1,48): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "isindex": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "isindex",
-                    "attrs": [
-                      {
-                        "name": "action",
-                        "value": "B"
-                      },
-                      {
-                        "name": "foo",
-                        "value": "D"
-                      },
-                      {
-                        "name": "name",
-                        "value": "A"
-                      },
-                      {
-                        "name": "prompt",
-                        "value": "C"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><isindex name=\"A\" action=\"B\" prompt=\"C\" foo=\"D\"></isindex></body></html>",
-        "noQuirksBodyHtml": "<isindex name=\"A\" action=\"B\" prompt=\"C\" foo=\"D\"></isindex>"
-      }
-    },
-    {
-      "data": "<form><isindex>",
-      "errors": [
-        "(1,6): expected-doctype-but-got-start-tag",
-        "(1,15): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "form": true,
-            "isindex": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "form",
-                    "children": [
-                      {
-                        "tag": "isindex"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><form><isindex></isindex></form></body></html>",
-        "noQuirksBodyHtml": "<form><isindex></isindex></form>"
-      }
-    },
-    {
-      "data": "<!doctype html><isindex>x</isindex>x",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "isindex": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "isindex",
-                    "children": [
-                      {
-                        "text": "x"
-                      }
-                    ]
-                  },
-                  {
-                    "text": "x"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><isindex>x</isindex>x</body></html>",
-        "noQuirksBodyHtml": "<isindex>x</isindex>x"
-      }
-    }
-  ],
-  "main-element.dat": [
-    {
-      "data": "<!doctype html><p>foo<main>bar<p>baz",
-      "errors": [
-        "(1,36): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "p": true,
-            "main": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "p",
-                    "children": [
-                      {
-                        "text": "foo"
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "main",
-                    "children": [
-                      {
-                        "text": "bar"
-                      },
-                      {
-                        "tag": "p",
-                        "children": [
-                          {
-                            "text": "baz"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><p>foo</p><main>bar<p>baz</p></main></body></html>",
-        "noQuirksBodyHtml": "<p>foo</p><main>bar<p>baz</p></main>"
-      }
-    },
-    {
-      "data": "<!doctype html><main><p>foo</main>bar",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "main": true,
-            "p": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "main",
-                    "children": [
-                      {
-                        "tag": "p",
-                        "children": [
-                          {
-                            "text": "foo"
-                          }
-                        ]
-                      }
-                    ]
-                  },
-                  {
-                    "text": "bar"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><main><p>foo</p></main>bar</body></html>",
-        "noQuirksBodyHtml": "<main><p>foo</p></main>bar"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html>xxx<svg><x><g><a><main><b>",
-      "errors": [
-        " * (1,42) unexpected HTML-like start tag token in foreign content",
-        " * (1,42) unexpected end of file"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "svg svg": true,
-            "svg x": true,
-            "svg g": true,
-            "svg a": true,
-            "svg main": true,
-            "b": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "xxx"
-                  },
-                  {
-                    "tag": "svg",
-                    "ns": "http://www.w3.org/2000/svg",
-                    "children": [
-                      {
-                        "tag": "x",
-                        "ns": "http://www.w3.org/2000/svg",
-                        "children": [
-                          {
-                            "tag": "g",
-                            "ns": "http://www.w3.org/2000/svg",
-                            "children": [
-                              {
-                                "tag": "a",
-                                "ns": "http://www.w3.org/2000/svg",
-                                "children": [
-                                  {
-                                    "tag": "main",
-                                    "ns": "http://www.w3.org/2000/svg"
-                                  }
-                                ]
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "b"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body>xxx<svg><x><g><a><main></main></a></g></x></svg><b></b></body></html>",
-        "noQuirksBodyHtml": "xxx<svg><x><g><a><main><b></b></main></a></g></x></svg>"
-      }
-    }
-  ],
-  "math.dat": [
-    {
-      "data": "<math><tr><td><mo><tr>",
-      "errors": [],
-      "fragment": {
-        "name": "td"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "math math": true,
-            "math tr": true,
-            "math td": true,
-            "math mo": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "math",
-            "ns": "http://www.w3.org/1998/Math/MathML",
-            "children": [
-              {
-                "tag": "tr",
-                "ns": "http://www.w3.org/1998/Math/MathML",
-                "children": [
-                  {
-                    "tag": "td",
-                    "ns": "http://www.w3.org/1998/Math/MathML",
-                    "children": [
-                      {
-                        "tag": "mo",
-                        "ns": "http://www.w3.org/1998/Math/MathML"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<math><tr><td><mo></mo></td></tr></math>",
-        "noQuirksBodyHtml": "<math><tr><td><mo></mo></td></tr></math>"
-      }
-    },
-    {
-      "data": "<math><tr><td><mo><tr>",
-      "errors": [],
-      "fragment": {
-        "name": "tr"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "math math": true,
-            "math tr": true,
-            "math td": true,
-            "math mo": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "math",
-            "ns": "http://www.w3.org/1998/Math/MathML",
-            "children": [
-              {
-                "tag": "tr",
-                "ns": "http://www.w3.org/1998/Math/MathML",
-                "children": [
-                  {
-                    "tag": "td",
-                    "ns": "http://www.w3.org/1998/Math/MathML",
-                    "children": [
-                      {
-                        "tag": "mo",
-                        "ns": "http://www.w3.org/1998/Math/MathML"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<math><tr><td><mo></mo></td></tr></math>",
-        "noQuirksBodyHtml": "<math><tr><td><mo></mo></td></tr></math>"
-      }
-    },
-    {
-      "data": "<math><thead><mo><tbody>",
-      "errors": [],
-      "fragment": {
-        "name": "thead"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "math math": true,
-            "math thead": true,
-            "math mo": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "math",
-            "ns": "http://www.w3.org/1998/Math/MathML",
-            "children": [
-              {
-                "tag": "thead",
-                "ns": "http://www.w3.org/1998/Math/MathML",
-                "children": [
-                  {
-                    "tag": "mo",
-                    "ns": "http://www.w3.org/1998/Math/MathML"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<math><thead><mo></mo></thead></math>",
-        "noQuirksBodyHtml": "<math><thead><mo></mo></thead></math>"
-      }
-    },
-    {
-      "data": "<math><tfoot><mo><tbody>",
-      "errors": [],
-      "fragment": {
-        "name": "tfoot"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "math math": true,
-            "math tfoot": true,
-            "math mo": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "math",
-            "ns": "http://www.w3.org/1998/Math/MathML",
-            "children": [
-              {
-                "tag": "tfoot",
-                "ns": "http://www.w3.org/1998/Math/MathML",
-                "children": [
-                  {
-                    "tag": "mo",
-                    "ns": "http://www.w3.org/1998/Math/MathML"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<math><tfoot><mo></mo></tfoot></math>",
-        "noQuirksBodyHtml": "<math><tfoot><mo></mo></tfoot></math>"
-      }
-    },
-    {
-      "data": "<math><tbody><mo><tfoot>",
-      "errors": [],
-      "fragment": {
-        "name": "tbody"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "math math": true,
-            "math tbody": true,
-            "math mo": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "math",
-            "ns": "http://www.w3.org/1998/Math/MathML",
-            "children": [
-              {
-                "tag": "tbody",
-                "ns": "http://www.w3.org/1998/Math/MathML",
-                "children": [
-                  {
-                    "tag": "mo",
-                    "ns": "http://www.w3.org/1998/Math/MathML"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<math><tbody><mo></mo></tbody></math>",
-        "noQuirksBodyHtml": "<math><tbody><mo></mo></tbody></math>"
-      }
-    },
-    {
-      "data": "<math><tbody><mo></table>",
-      "errors": [],
-      "fragment": {
-        "name": "tbody"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "math math": true,
-            "math tbody": true,
-            "math mo": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "math",
-            "ns": "http://www.w3.org/1998/Math/MathML",
-            "children": [
-              {
-                "tag": "tbody",
-                "ns": "http://www.w3.org/1998/Math/MathML",
-                "children": [
-                  {
-                    "tag": "mo",
-                    "ns": "http://www.w3.org/1998/Math/MathML"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<math><tbody><mo></mo></tbody></math>",
-        "noQuirksBodyHtml": "<math><tbody><mo></mo></tbody></math>"
-      }
-    },
-    {
-      "data": "<math><thead><mo></table>",
-      "errors": [],
-      "fragment": {
-        "name": "tbody"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "math math": true,
-            "math thead": true,
-            "math mo": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "math",
-            "ns": "http://www.w3.org/1998/Math/MathML",
-            "children": [
-              {
-                "tag": "thead",
-                "ns": "http://www.w3.org/1998/Math/MathML",
-                "children": [
-                  {
-                    "tag": "mo",
-                    "ns": "http://www.w3.org/1998/Math/MathML"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<math><thead><mo></mo></thead></math>",
-        "noQuirksBodyHtml": "<math><thead><mo></mo></thead></math>"
-      }
-    },
-    {
-      "data": "<math><tfoot><mo></table>",
-      "errors": [],
-      "fragment": {
-        "name": "tbody"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "math math": true,
-            "math tfoot": true,
-            "math mo": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "math",
-            "ns": "http://www.w3.org/1998/Math/MathML",
-            "children": [
-              {
-                "tag": "tfoot",
-                "ns": "http://www.w3.org/1998/Math/MathML",
-                "children": [
-                  {
-                    "tag": "mo",
-                    "ns": "http://www.w3.org/1998/Math/MathML"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<math><tfoot><mo></mo></tfoot></math>",
-        "noQuirksBodyHtml": "<math><tfoot><mo></mo></tfoot></math>"
-      }
-    }
-  ],
-  "menuitem-element.dat": [
-    {
-      "data": "<menuitem>",
-      "errors": [
-        "10: Start tag seen without seeing a doctype first. Expected “<!DOCTYPE html>”."
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "menuitem": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "menuitem"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><menuitem></menuitem></body></html>",
-        "noQuirksBodyHtml": "<menuitem></menuitem>"
-      }
-    },
-    {
-      "data": "</menuitem>",
-      "errors": [
-        "11: End tag seen without seeing a doctype first. Expected “<!DOCTYPE html>”.",
-        "11: Stray end tag “menuitem”."
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body></body></html>",
-        "noQuirksBodyHtml": ""
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><body><menuitem>A",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "menuitem": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "menuitem",
-                    "children": [
-                      {
-                        "text": "A"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><menuitem>A</menuitem></body></html>",
-        "noQuirksBodyHtml": "<menuitem>A</menuitem>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><body><menuitem>A<menuitem>B",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "menuitem": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "menuitem",
-                    "children": [
-                      {
-                        "text": "A"
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "menuitem",
-                    "children": [
-                      {
-                        "text": "B"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><menuitem>A</menuitem><menuitem>B</menuitem></body></html>",
-        "noQuirksBodyHtml": "<menuitem>A</menuitem><menuitem>B</menuitem>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><body><menuitem>A<menu>B</menu>",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "menuitem": true,
-            "menu": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "menuitem",
-                    "children": [
-                      {
-                        "text": "A"
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "menu",
-                    "children": [
-                      {
-                        "text": "B"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><menuitem>A</menuitem><menu>B</menu></body></html>",
-        "noQuirksBodyHtml": "<menuitem>A</menuitem><menu>B</menu>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><body><menuitem>A<hr>B",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "menuitem": true,
-            "hr": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "menuitem",
-                    "children": [
-                      {
-                        "text": "A"
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "hr"
-                  },
-                  {
-                    "text": "B"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><menuitem>A</menuitem><hr>B</body></html>",
-        "noQuirksBodyHtml": "<menuitem>A</menuitem><hr>B"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><li><menuitem><li>",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "li": true,
-            "menuitem": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "li",
-                    "children": [
-                      {
-                        "tag": "menuitem"
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "li"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><li><menuitem></menuitem></li><li></li></body></html>",
-        "noQuirksBodyHtml": "<li><menuitem></menuitem></li><li></li>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><menuitem><p></menuitem>x",
-      "errors": [
-        "39: Stray end tag “menuitem”."
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "menuitem": true,
-            "p": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "menuitem",
-                    "children": [
-                      {
-                        "tag": "p",
-                        "children": [
-                          {
-                            "text": "x"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><menuitem><p>x</p></menuitem></body></html>",
-        "noQuirksBodyHtml": "<menuitem><p>x</p></menuitem>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><p><b></p><menuitem>",
-      "errors": [
-        "25: End tag “p” seen, but there were open elements.",
-        "21: Unclosed element “b”.",
-        "35: End of file seen and there were open elements."
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "p": true,
-            "b": true,
-            "menuitem": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "p",
-                    "children": [
-                      {
-                        "tag": "b"
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "b",
-                    "children": [
-                      {
-                        "tag": "menuitem"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><p><b></b></p><b><menuitem></menuitem></b></body></html>",
-        "noQuirksBodyHtml": "<p><b></b></p><b><menuitem></menuitem></b>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><menuitem><asdf></menuitem>x",
-      "errors": [
-        "40: End tag “menuitem” seen, but there were open elements.",
-        "31: Unclosed element “asdf”."
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "menuitem": true,
-            "asdf": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "menuitem",
-                    "children": [
-                      {
-                        "tag": "asdf"
-                      }
-                    ]
-                  },
-                  {
-                    "text": "x"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><menuitem><asdf></asdf></menuitem>x</body></html>",
-        "noQuirksBodyHtml": "<menuitem><asdf></asdf></menuitem>x"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html></menuitem>",
-      "errors": [
-        "26: Stray end tag “menuitem”."
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body></body></html>",
-        "noQuirksBodyHtml": ""
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><html></menuitem>",
-      "errors": [
-        "26: Stray end tag “menuitem”."
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body></body></html>",
-        "noQuirksBodyHtml": ""
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><head></menuitem>",
-      "errors": [
-        "26: Stray end tag “menuitem”."
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body></body></html>",
-        "noQuirksBodyHtml": ""
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><select><menuitem></select>",
-      "errors": [
-        "33: Stray start tag “menuitem”."
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "select": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "select"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><select></select></body></html>",
-        "noQuirksBodyHtml": "<select></select>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><option><menuitem>",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "option": true,
-            "menuitem": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "option",
-                    "children": [
-                      {
-                        "tag": "menuitem"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><option><menuitem></menuitem></option></body></html>",
-        "noQuirksBodyHtml": "<option><menuitem></menuitem></option>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><menuitem><option>",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "menuitem": true,
-            "option": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "menuitem",
-                    "children": [
-                      {
-                        "tag": "option"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><menuitem><option></option></menuitem></body></html>",
-        "noQuirksBodyHtml": "<menuitem><option></option></menuitem>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><menuitem></body>",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "menuitem": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "menuitem"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><menuitem></menuitem></body></html>",
-        "noQuirksBodyHtml": "<menuitem></menuitem>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><menuitem></html>",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "menuitem": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "menuitem"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><menuitem></menuitem></body></html>",
-        "noQuirksBodyHtml": "<menuitem></menuitem>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><menuitem><p>",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "menuitem": true,
-            "p": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "menuitem",
-                    "children": [
-                      {
-                        "tag": "p"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><menuitem><p></p></menuitem></body></html>",
-        "noQuirksBodyHtml": "<menuitem><p></p></menuitem>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><menuitem><li>",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "menuitem": true,
-            "li": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "menuitem",
-                    "children": [
-                      {
-                        "tag": "li"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><menuitem><li></li></menuitem></body></html>",
-        "noQuirksBodyHtml": "<menuitem><li></li></menuitem>"
-      }
-    }
-  ],
-  "namespace-sensitivity.dat": [
-    {
-      "data": "<body><table><tr><td><svg><td><foreignObject><span></td>Foo",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true,
-            "tbody": true,
-            "tr": true,
-            "td": true,
-            "svg svg": true,
-            "svg td": true,
-            "svg foreignObject": true,
-            "span": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "Foo"
-                  },
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "tbody",
-                        "children": [
-                          {
-                            "tag": "tr",
-                            "children": [
-                              {
-                                "tag": "td",
-                                "children": [
-                                  {
-                                    "tag": "svg",
-                                    "ns": "http://www.w3.org/2000/svg",
-                                    "children": [
-                                      {
-                                        "tag": "td",
-                                        "ns": "http://www.w3.org/2000/svg",
-                                        "children": [
-                                          {
-                                            "tag": "foreignObject",
-                                            "ns": "http://www.w3.org/2000/svg",
-                                            "children": [
-                                              {
-                                                "tag": "span"
-                                              }
-                                            ]
-                                          }
-                                        ]
-                                      }
-                                    ]
-                                  }
-                                ]
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>Foo<table><tbody><tr><td><svg><td><foreignObject><span></span></foreignObject></td></svg></td></tr></tbody></table></body></html>",
-        "noQuirksBodyHtml": "Foo<table><tbody><tr><td><svg><td><foreignObject><span></span></foreignObject></td></svg></td></tr></tbody></table>"
-      }
-    }
-  ],
-  "noscript01.dat": [
-    {
-      "data": "<head><noscript><!doctype html><!--foo--></noscript>",
-      "errors": [
-        "Line: 1 Col: 6 Unexpected start tag (head). Expected DOCTYPE.",
-        "Line: 1 Col: 31 Unexpected DOCTYPE. Ignored."
-      ],
-      "script": "off",
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "noscript": true,
-            "body": true
-          },
-          "comment": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "noscript",
-                    "children": [
-                      {
-                        "comment": "foo"
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><noscript><!--foo--></noscript></head><body></body></html>",
-        "noQuirksBodyHtml": "<noscript><!--foo--></noscript>"
-      }
-    },
-    {
-      "data": "<head><noscript><html class=\"foo\"><!--foo--></noscript>",
-      "errors": [
-        "Line: 1 Col: 6 Unexpected start tag (head). Expected DOCTYPE.",
-        "Line: 1 Col: 34 html needs to be the first start tag."
-      ],
-      "script": "off",
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "noscript": true,
-            "body": true
-          },
-          "comment": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "attrs": [
-              {
-                "name": "class",
-                "value": "foo"
-              }
-            ],
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "noscript",
-                    "children": [
-                      {
-                        "comment": "foo"
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html class=\"foo\"><head><noscript><!--foo--></noscript></head><body></body></html>",
-        "noQuirksBodyHtml": "<noscript><!--foo--></noscript>"
-      }
-    },
-    {
-      "data": "<head><noscript></noscript>",
-      "errors": [
-        "(1,6): expected-doctype-but-got-tag"
-      ],
-      "script": "off",
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "noscript": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "noscript"
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><noscript></noscript></head><body></body></html>",
-        "noQuirksBodyHtml": "<noscript></noscript>"
-      }
-    },
-    {
-      "data": "<head><noscript>   </noscript>",
-      "errors": [
-        "Line: 1 Col: 6 Unexpected start tag (head). Expected DOCTYPE."
-      ],
-      "script": "off",
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "noscript": true,
-            "body": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "noscript",
-                    "children": [
-                      {
-                        "text": "   ",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><noscript>   </noscript></head><body></body></html>",
-        "noQuirksBodyHtml": "<noscript>   </noscript>"
-      }
-    },
-    {
-      "data": "<head><noscript><!--foo--></noscript>",
-      "errors": [
-        "(1,6): expected-doctype-but-got-tag"
-      ],
-      "script": "off",
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "noscript": true,
-            "body": true
-          },
-          "comment": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "noscript",
-                    "children": [
-                      {
-                        "comment": "foo"
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><noscript><!--foo--></noscript></head><body></body></html>",
-        "noQuirksBodyHtml": "<noscript><!--foo--></noscript>"
-      }
-    },
-    {
-      "data": "<head><noscript><basefont><!--foo--></noscript>",
-      "errors": [
-        "Line: 1 Col: 6 Unexpected start tag (head). Expected DOCTYPE."
-      ],
-      "script": "off",
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "noscript": true,
-            "basefont": true,
-            "body": true
-          },
-          "comment": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "noscript",
-                    "children": [
-                      {
-                        "tag": "basefont"
-                      },
-                      {
-                        "comment": "foo"
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><noscript><basefont><!--foo--></noscript></head><body></body></html>",
-        "noQuirksBodyHtml": "<noscript><basefont><!--foo--></noscript>"
-      }
-    },
-    {
-      "data": "<head><noscript><bgsound><!--foo--></noscript>",
-      "errors": [
-        "Line: 1 Col: 6 Unexpected start tag (head). Expected DOCTYPE."
-      ],
-      "script": "off",
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "noscript": true,
-            "bgsound": true,
-            "body": true
-          },
-          "comment": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "noscript",
-                    "children": [
-                      {
-                        "tag": "bgsound"
-                      },
-                      {
-                        "comment": "foo"
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><noscript><bgsound><!--foo--></noscript></head><body></body></html>",
-        "noQuirksBodyHtml": "<noscript><bgsound><!--foo--></noscript>"
-      }
-    },
-    {
-      "data": "<head><noscript><link><!--foo--></noscript>",
-      "errors": [
-        "Line: 1 Col: 6 Unexpected start tag (head). Expected DOCTYPE."
-      ],
-      "script": "off",
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "noscript": true,
-            "link": true,
-            "body": true
-          },
-          "comment": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "noscript",
-                    "children": [
-                      {
-                        "tag": "link"
-                      },
-                      {
-                        "comment": "foo"
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><noscript><link><!--foo--></noscript></head><body></body></html>",
-        "noQuirksBodyHtml": "<noscript><link><!--foo--></noscript>"
-      }
-    },
-    {
-      "data": "<head><noscript><meta><!--foo--></noscript>",
-      "errors": [
-        "Line: 1 Col: 6 Unexpected start tag (head). Expected DOCTYPE."
-      ],
-      "script": "off",
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "noscript": true,
-            "meta": true,
-            "body": true
-          },
-          "comment": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "noscript",
-                    "children": [
-                      {
-                        "tag": "meta"
-                      },
-                      {
-                        "comment": "foo"
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><noscript><meta><!--foo--></noscript></head><body></body></html>",
-        "noQuirksBodyHtml": "<noscript><meta><!--foo--></noscript>"
-      }
-    },
-    {
-      "data": "<head><noscript><noframes>XXX</noscript></noframes></noscript>",
-      "errors": [
-        "Line: 1 Col: 6 Unexpected start tag (head). Expected DOCTYPE."
-      ],
-      "script": "off",
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "noscript": true,
-            "noframes": true,
-            "body": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "noscript",
-                    "children": [
-                      {
-                        "tag": "noframes",
-                        "children": [
-                          {
-                            "text": "XXX</noscript>",
-                            "no_escape": true
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><noscript><noframes>XXX</noscript></noframes></noscript></head><body></body></html>",
-        "noQuirksBodyHtml": "<noscript><noframes>XXX</noscript></noframes></noscript>"
-      }
-    },
-    {
-      "data": "<head><noscript><style>XXX</style></noscript>",
-      "errors": [
-        "Line: 1 Col: 6 Unexpected start tag (head). Expected DOCTYPE."
-      ],
-      "script": "off",
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "noscript": true,
-            "style": true,
-            "body": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "noscript",
-                    "children": [
-                      {
-                        "tag": "style",
-                        "children": [
-                          {
-                            "text": "XXX",
-                            "no_escape": true
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><noscript><style>XXX</style></noscript></head><body></body></html>",
-        "noQuirksBodyHtml": "<noscript><style>XXX</style></noscript>"
-      }
-    },
-    {
-      "data": "<head><noscript></br><!--foo--></noscript>",
-      "errors": [
-        "Line: 1 Col: 6 Unexpected start tag (head). Expected DOCTYPE.",
-        "Line: 1 Col: 21 Element br not allowed in a inhead-noscript context",
-        "Line: 1 Col: 21 Unexpected end tag (br). Treated as br element.",
-        "Line: 1 Col: 42 Unexpected end tag (noscript). Ignored."
-      ],
-      "script": "off",
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "noscript": true,
-            "body": true,
-            "br": true
-          },
-          "comment": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "noscript"
-                  }
-                ]
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "br"
-                  },
-                  {
-                    "comment": "foo"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><noscript></noscript></head><body><br><!--foo--></body></html>",
-        "noQuirksBodyHtml": "<noscript><br><!--foo--></noscript>"
-      }
-    },
-    {
-      "data": "<head><noscript><head class=\"foo\"><!--foo--></noscript>",
-      "errors": [
-        "Line: 1 Col: 6 Unexpected start tag (head). Expected DOCTYPE.",
-        "Line: 1 Col: 34 Unexpected start tag (head)."
-      ],
-      "script": "off",
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "noscript": true,
-            "body": true
-          },
-          "comment": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "noscript",
-                    "children": [
-                      {
-                        "comment": "foo"
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><noscript><!--foo--></noscript></head><body></body></html>",
-        "noQuirksBodyHtml": "<noscript><!--foo--></noscript>"
-      }
-    },
-    {
-      "data": "<head><noscript><noscript class=\"foo\"><!--foo--></noscript>",
-      "errors": [
-        "Line: 1 Col: 6 Unexpected start tag (head). Expected DOCTYPE.",
-        "Line: 1 Col: 34 Unexpected start tag (noscript)."
-      ],
-      "script": "off",
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "noscript": true,
-            "body": true
-          },
-          "comment": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "noscript",
-                    "children": [
-                      {
-                        "comment": "foo"
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><noscript><!--foo--></noscript></head><body></body></html>",
-        "noQuirksBodyHtml": "<noscript><noscript class=\"foo\"><!--foo--></noscript></noscript>"
-      }
-    },
-    {
-      "data": "<head><noscript></p><!--foo--></noscript>",
-      "errors": [
-        "Line: 1 Col: 6 Unexpected start tag (head). Expected DOCTYPE.",
-        "Line: 1 Col: 20 Unexpected end tag (p). Ignored."
-      ],
-      "script": "off",
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "noscript": true,
-            "body": true
-          },
-          "comment": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "noscript",
-                    "children": [
-                      {
-                        "comment": "foo"
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><noscript><!--foo--></noscript></head><body></body></html>",
-        "noQuirksBodyHtml": "<noscript><p></p><!--foo--></noscript>"
-      }
-    },
-    {
-      "data": "<head><noscript><p><!--foo--></noscript>",
-      "errors": [
-        "Line: 1 Col: 6 Unexpected start tag (head). Expected DOCTYPE.",
-        "Line: 1 Col: 19 Element p not allowed in a inhead-noscript context",
-        "Line: 1 Col: 40 Unexpected end tag (noscript). Ignored."
-      ],
-      "script": "off",
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "noscript": true,
-            "body": true,
-            "p": true
-          },
-          "comment": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "noscript"
-                  }
-                ]
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "p",
-                    "children": [
-                      {
-                        "comment": "foo"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><noscript></noscript></head><body><p><!--foo--></p></body></html>",
-        "noQuirksBodyHtml": "<noscript><p><!--foo--></p></noscript>"
-      }
-    },
-    {
-      "data": "<head><noscript>XXX<!--foo--></noscript></head>",
-      "errors": [
-        "Line: 1 Col: 6 Unexpected start tag (head). Expected DOCTYPE.",
-        "Line: 1 Col: 19 Unexpected non-space character. Expected inhead-noscript content",
-        "Line: 1 Col: 30 Unexpected end tag (noscript). Ignored.",
-        "Line: 1 Col: 37 Unexpected end tag (head). Ignored."
-      ],
-      "script": "off",
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "noscript": true,
-            "body": true
-          },
-          "comment": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "noscript"
-                  }
-                ]
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "XXX"
-                  },
-                  {
-                    "comment": "foo"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><noscript></noscript></head><body>XXX<!--foo--></body></html>",
-        "noQuirksBodyHtml": "<noscript>XXX<!--foo--></noscript>"
-      }
-    },
-    {
-      "data": "<head><noscript>",
-      "errors": [
-        "(1,6): expected-doctype-but-got-tag",
-        "(1,6): eof-in-head-noscript"
-      ],
-      "script": "off",
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "noscript": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "noscript"
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><noscript></noscript></head><body></body></html>",
-        "noQuirksBodyHtml": "<noscript></noscript>"
-      }
-    }
-  ],
-  "pending-spec-changes-plain-text-unsafe.dat": [
-    {
-      "data": "<body><table>\u0000filler\u0000text\u0000",
-      "errors": [
-        "(1,6): expected-doctype-but-got-start-tag",
-        "(1,14): invalid-codepoint",
-        "(1,14): invalid-codepoint-in-table-text",
-        "(1,21): invalid-codepoint",
-        "(1,21): invalid-codepoint-in-table-text",
-        "(1,26): invalid-codepoint",
-        "(1,26): invalid-codepoint-in-table-text",
-        "(1,26): foster-parenting-character-in-table",
-        "(1,26): foster-parenting-character-in-table",
-        "(1,26): foster-parenting-character-in-table",
-        "(1,26): foster-parenting-character-in-table",
-        "(1,26): foster-parenting-character-in-table",
-        "(1,26): foster-parenting-character-in-table",
-        "(1,26): foster-parenting-character-in-table",
-        "(1,26): foster-parenting-character-in-table",
-        "(1,26): foster-parenting-character-in-table",
-        "(1,26): foster-parenting-character-in-table",
-        "(1,26): eof-in-table"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "fillertext"
-                  },
-                  {
-                    "tag": "table"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>fillertext<table></table></body></html>",
-        "noQuirksBodyHtml": "fillertext<table></table>"
-      }
-    }
-  ],
-  "pending-spec-changes.dat": [
-    {
-      "data": "<input type=\"hidden\"><frameset>",
-      "errors": [
-        "(1,21): expected-doctype-but-got-start-tag",
-        "(1,31): unexpected-start-tag",
-        "(1,31): eof-in-frameset"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "frameset": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "frameset"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><frameset></frameset></html>",
-        "noQuirksBodyHtml": "<input type=\"hidden\">"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><table><caption><svg>foo</table>bar",
-      "errors": [
-        "(1,47): unexpected-end-tag",
-        "(1,47): end-table-tag-in-caption"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true,
-            "caption": true,
-            "svg svg": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "caption",
-                        "children": [
-                          {
-                            "tag": "svg",
-                            "ns": "http://www.w3.org/2000/svg",
-                            "children": [
-                              {
-                                "text": "foo"
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  },
-                  {
-                    "text": "bar"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><table><caption><svg>foo</svg></caption></table>bar</body></html>",
-        "noQuirksBodyHtml": "<table><caption><svg>foo</svg></caption></table>bar"
-      }
-    },
-    {
-      "data": "<table><tr><td><svg><desc><td></desc><circle>",
-      "errors": [
-        "(1,7): expected-doctype-but-got-start-tag",
-        "(1,30): unexpected-cell-end-tag",
-        "(1,37): unexpected-end-tag",
-        "(1,45): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true,
-            "tbody": true,
-            "tr": true,
-            "td": true,
-            "svg svg": true,
-            "svg desc": true,
-            "circle": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "tbody",
-                        "children": [
-                          {
-                            "tag": "tr",
-                            "children": [
-                              {
-                                "tag": "td",
-                                "children": [
-                                  {
-                                    "tag": "svg",
-                                    "ns": "http://www.w3.org/2000/svg",
-                                    "children": [
-                                      {
-                                        "tag": "desc",
-                                        "ns": "http://www.w3.org/2000/svg"
-                                      }
-                                    ]
-                                  }
-                                ]
-                              },
-                              {
-                                "tag": "td",
-                                "children": [
-                                  {
-                                    "tag": "circle"
-                                  }
-                                ]
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><table><tbody><tr><td><svg><desc></desc></svg></td><td><circle></circle></td></tr></tbody></table></body></html>",
-        "noQuirksBodyHtml": "<table><tbody><tr><td><svg><desc></desc></svg></td><td><circle></circle></td></tr></tbody></table>"
-      }
-    }
-  ],
-  "plain-text-unsafe.dat": [
-    {
-      "data": "FOO&#x000D;ZOO",
-      "errors": [
-        "(1,3): expected-doctype-but-got-chars",
-        "(1,11): illegal-codepoint-for-numeric-entity"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "FOO\rZOO"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>FOO\rZOO</body></html>",
-        "noQuirksBodyHtml": "FOO\rZOO"
-      }
-    },
-    {
-      "data": "<html>\u0000<frameset></frameset>",
-      "errors": [
-        "(1,6): expected-doctype-but-got-start-tag",
-        "(1,7): invalid-codepoint",
-        "(1,7): invalid-codepoint-in-body",
-        "(1,17): unexpected-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "frameset": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "frameset"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><frameset></frameset></html>",
-        "noQuirksBodyHtml": ""
-      }
-    },
-    {
-      "data": "<html> \u0000 <frameset></frameset>",
-      "errors": [
-        "(1,6): expected-doctype-but-got-start-tag",
-        "(1,8): invalid-codepoint",
-        "(1,8): invalid-codepoint-in-body",
-        "(1,19): unexpected-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "frameset": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "frameset"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><frameset></frameset></html>",
-        "noQuirksBodyHtml": "  "
-      }
-    },
-    {
-      "data": "<html>a\u0000a<frameset></frameset>",
-      "errors": [
-        "(1,6): expected-doctype-but-got-start-tag",
-        "(1,8): invalid-codepoint",
-        "(1,8): invalid-codepoint-in-body",
-        "(1,19): unexpected-start-tag",
-        "(1,30): unexpected-end-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "aa"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>aa</body></html>",
-        "noQuirksBodyHtml": "aa"
-      }
-    },
-    {
-      "data": "<html>\u0000\u0000<frameset></frameset>",
-      "errors": [
-        "(1,6): expected-doctype-but-got-start-tag",
-        "(1,7): invalid-codepoint",
-        "(1,7): invalid-codepoint-in-body",
-        "(1,8): invalid-codepoint",
-        "(1,8): invalid-codepoint-in-body",
-        "(1,18): unexpected-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "frameset": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "frameset"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><frameset></frameset></html>",
-        "noQuirksBodyHtml": ""
-      }
-    },
-    {
-      "data": "<html>\u0000\n <frameset></frameset>",
-      "errors": [
-        "(1,6): expected-doctype-but-got-start-tag",
-        "(1,7): invalid-codepoint",
-        "(1,7): invalid-codepoint-in-body",
-        "(2,11): unexpected-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "frameset": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "frameset"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><frameset></frameset></html>",
-        "noQuirksBodyHtml": "\n "
-      }
-    },
-    {
-      "data": "<html><select>\u0000",
-      "errors": [
-        "(1,6): expected-doctype-but-got-start-tag",
-        "(1,15): invalid-codepoint",
-        "(1,15): invalid-codepoint-in-select",
-        "(1,15): eof-in-select"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "select": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "select"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><select></select></body></html>",
-        "noQuirksBodyHtml": "<select></select>"
-      }
-    },
-    {
-      "data": "\u0000",
-      "errors": [
-        "(1,1): invalid-codepoint",
-        "(1,1): expected-doctype-but-got-chars",
-        "(1,1): invalid-codepoint-in-body"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body></body></html>",
-        "noQuirksBodyHtml": ""
-      }
-    },
-    {
-      "data": "<body>\u0000",
-      "errors": [
-        "(1,6): expected-doctype-but-got-start-tag",
-        "(1,7): invalid-codepoint",
-        "(1,7): invalid-codepoint-in-body"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body></body></html>",
-        "noQuirksBodyHtml": ""
-      }
-    },
-    {
-      "data": "<plaintext>\u0000filler\u0000text\u0000",
-      "errors": [
-        "(1,11): expected-doctype-but-got-start-tag",
-        "(1,12): invalid-codepoint",
-        "(1,19): invalid-codepoint",
-        "(1,24): invalid-codepoint",
-        "(1,24): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "plaintext": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "plaintext",
-                    "children": [
-                      {
-                        "text": "�filler�text�",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><plaintext>�filler�text�</plaintext></body></html>",
-        "noQuirksBodyHtml": "<plaintext>�filler�text�</plaintext>"
-      }
-    },
-    {
-      "data": "<svg><![CDATA[\u0000filler\u0000text\u0000]]>",
-      "errors": [
-        "(1,5): expected-doctype-but-got-start-tag",
-        "(1,30): invalid-codepoint",
-        "(1,30): invalid-codepoint",
-        "(1,30): invalid-codepoint",
-        "(1,30): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "svg svg": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "svg",
-                    "ns": "http://www.w3.org/2000/svg",
-                    "children": [
-                      {
-                        "text": "�filler�text�"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><svg>�filler�text�</svg></body></html>",
-        "noQuirksBodyHtml": "<svg>�filler�text�</svg>"
-      }
-    },
-    {
-      "data": "<body><!\u0000>",
-      "errors": [
-        "(1,6): expected-doctype-but-got-start-tag",
-        "(1,8): expected-dashes-or-doctype"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "comment": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "comment": "�"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><!--�--></body></html>",
-        "noQuirksBodyHtml": "<!--�-->"
-      }
-    },
-    {
-      "data": "<body><!\u0000filler\u0000text>",
-      "errors": [
-        "(1,6): expected-doctype-but-got-start-tag",
-        "(1,8): expected-dashes-or-doctype"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "comment": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "comment": "�filler�text"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><!--�filler�text--></body></html>",
-        "noQuirksBodyHtml": "<!--�filler�text-->"
-      }
-    },
-    {
-      "data": "<body><svg><foreignObject>\u0000filler\u0000text",
-      "errors": [
-        "(1,6): expected-doctype-but-got-start-tag",
-        "(1,27): invalid-codepoint",
-        "(1,27): invalid-codepoint-in-body",
-        "(1,34): invalid-codepoint",
-        "(1,34): invalid-codepoint-in-body",
-        "(1,38): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "svg svg": true,
-            "svg foreignObject": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "svg",
-                    "ns": "http://www.w3.org/2000/svg",
-                    "children": [
-                      {
-                        "tag": "foreignObject",
-                        "ns": "http://www.w3.org/2000/svg",
-                        "children": [
-                          {
-                            "text": "fillertext"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><svg><foreignObject>fillertext</foreignObject></svg></body></html>",
-        "noQuirksBodyHtml": "<svg><foreignObject>fillertext</foreignObject></svg>"
-      }
-    },
-    {
-      "data": "<svg>\u0000filler\u0000text",
-      "errors": [
-        "(1,5): expected-doctype-but-got-start-tag",
-        "(1,6): invalid-codepoint",
-        "(1,6): invalid-codepoint-in-foreign-content",
-        "(1,13): invalid-codepoint",
-        "(1,13): invalid-codepoint-in-foreign-content",
-        "(1,17): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "svg svg": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "svg",
-                    "ns": "http://www.w3.org/2000/svg",
-                    "children": [
-                      {
-                        "text": "�filler�text"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><svg>�filler�text</svg></body></html>",
-        "noQuirksBodyHtml": "<svg>�filler�text</svg>"
-      }
-    },
-    {
-      "data": "<svg>\u0000<frameset>",
-      "errors": [
-        "(1,5): expected-doctype-but-got-start-tag",
-        "(1,6): invalid-codepoint",
-        "(1,6): invalid-codepoint-in-foreign-content",
-        "(1,16): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "svg svg": true,
-            "svg frameset": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "svg",
-                    "ns": "http://www.w3.org/2000/svg",
-                    "children": [
-                      {
-                        "text": "�"
-                      },
-                      {
-                        "tag": "frameset",
-                        "ns": "http://www.w3.org/2000/svg"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><svg>�<frameset></frameset></svg></body></html>",
-        "noQuirksBodyHtml": "<svg>�<frameset></frameset></svg>"
-      }
-    },
-    {
-      "data": "<svg>\u0000 <frameset>",
-      "errors": [
-        "(1,5): expected-doctype-but-got-start-tag",
-        "(1,6): invalid-codepoint",
-        "(1,6): invalid-codepoint-in-foreign-content",
-        "(1,17): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "svg svg": true,
-            "svg frameset": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "svg",
-                    "ns": "http://www.w3.org/2000/svg",
-                    "children": [
-                      {
-                        "text": "� "
-                      },
-                      {
-                        "tag": "frameset",
-                        "ns": "http://www.w3.org/2000/svg"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><svg>� <frameset></frameset></svg></body></html>",
-        "noQuirksBodyHtml": "<svg>� <frameset></frameset></svg>"
-      }
-    },
-    {
-      "data": "<svg>\u0000a<frameset>",
-      "errors": [
-        "(1,5): expected-doctype-but-got-start-tag",
-        "(1,6): invalid-codepoint",
-        "(1,6): invalid-codepoint-in-foreign-content",
-        "(1,17): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "svg svg": true,
-            "svg frameset": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "svg",
-                    "ns": "http://www.w3.org/2000/svg",
-                    "children": [
-                      {
-                        "text": "�a"
-                      },
-                      {
-                        "tag": "frameset",
-                        "ns": "http://www.w3.org/2000/svg"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><svg>�a<frameset></frameset></svg></body></html>",
-        "noQuirksBodyHtml": "<svg>�a<frameset></frameset></svg>"
-      }
-    },
-    {
-      "data": "<svg>\u0000</svg><frameset>",
-      "errors": [
-        "(1,5): expected-doctype-but-got-start-tag",
-        "(1,6): invalid-codepoint",
-        "(1,6): invalid-codepoint-in-foreign-content",
-        "(1,22): unexpected-start-tag",
-        "(1,22): eof-in-frameset"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "frameset": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "frameset"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><frameset></frameset></html>",
-        "noQuirksBodyHtml": "<svg>�</svg>"
-      }
-    },
-    {
-      "data": "<svg>\u0000 </svg><frameset>",
-      "errors": [
-        "(1,5): expected-doctype-but-got-start-tag",
-        "(1,6): invalid-codepoint",
-        "(1,6): invalid-codepoint-in-foreign-content",
-        "(1,23): unexpected-start-tag",
-        "(1,23): eof-in-frameset"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "frameset": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "frameset"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><frameset></frameset></html>",
-        "noQuirksBodyHtml": "<svg>� </svg>"
-      }
-    },
-    {
-      "data": "<svg>\u0000a</svg><frameset>",
-      "errors": [
-        "(1,5): expected-doctype-but-got-start-tag",
-        "(1,6): invalid-codepoint",
-        "(1,6): invalid-codepoint-in-foreign-content",
-        "(1,23): unexpected-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "svg svg": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "svg",
-                    "ns": "http://www.w3.org/2000/svg",
-                    "children": [
-                      {
-                        "text": "�a"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><svg>�a</svg></body></html>",
-        "noQuirksBodyHtml": "<svg>�a</svg>"
-      }
-    },
-    {
-      "data": "<svg><path></path></svg><frameset>",
-      "errors": [
-        "(1,5): expected-doctype-but-got-start-tag",
-        "(1,34): unexpected-start-tag",
-        "(1,34): eof-in-frameset"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "frameset": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "frameset"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><frameset></frameset></html>",
-        "noQuirksBodyHtml": "<svg><path></path></svg>"
-      }
-    },
-    {
-      "data": "<svg><p><frameset>",
-      "errors": [
-        "(1, 5) expected-doctype-but-got-start-tag",
-        "(1, 8) unexpected-html-element-in-foreign-content",
-        "(1, 18) unexpected-start-tag",
-        "(1, 18) eof-in-frameset"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "frameset": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "frameset"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><frameset></frameset></html>",
-        "noQuirksBodyHtml": "<svg><p><frameset></frameset></p></svg>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><pre>\r\n\r\nA</pre>",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "pre": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "pre",
-                    "children": [
-                      {
-                        "text": "\nA"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><pre>\nA</pre></body></html>",
-        "noQuirksBodyHtml": "<pre>\nA</pre>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><pre>\r\rA</pre>",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "pre": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "pre",
-                    "children": [
-                      {
-                        "text": "\nA"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><pre>\nA</pre></body></html>",
-        "noQuirksBodyHtml": "<pre>\nA</pre>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><pre>\rA</pre>",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "pre": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "pre",
-                    "children": [
-                      {
-                        "text": "A"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><pre>A</pre></body></html>",
-        "noQuirksBodyHtml": "<pre>A</pre>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><table><tr><td><math><mtext>\u0000a",
-      "errors": [
-        "(1,44): invalid-codepoint",
-        "(1,44): invalid-codepoint-in-body",
-        "(1,45): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true,
-            "tbody": true,
-            "tr": true,
-            "td": true,
-            "math math": true,
-            "math mtext": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "tbody",
-                        "children": [
-                          {
-                            "tag": "tr",
-                            "children": [
-                              {
-                                "tag": "td",
-                                "children": [
-                                  {
-                                    "tag": "math",
-                                    "ns": "http://www.w3.org/1998/Math/MathML",
-                                    "children": [
-                                      {
-                                        "tag": "mtext",
-                                        "ns": "http://www.w3.org/1998/Math/MathML",
-                                        "children": [
-                                          {
-                                            "text": "a"
-                                          }
-                                        ]
-                                      }
-                                    ]
-                                  }
-                                ]
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><table><tbody><tr><td><math><mtext>a</mtext></math></td></tr></tbody></table></body></html>",
-        "noQuirksBodyHtml": "<table><tbody><tr><td><math><mtext>a</mtext></math></td></tr></tbody></table>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><table><tr><td><svg><foreignObject>\u0000a",
-      "errors": [
-        "(1,51): invalid-codepoint",
-        "(1,51): invalid-codepoint-in-body",
-        "(1,52): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true,
-            "tbody": true,
-            "tr": true,
-            "td": true,
-            "svg svg": true,
-            "svg foreignObject": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "tbody",
-                        "children": [
-                          {
-                            "tag": "tr",
-                            "children": [
-                              {
-                                "tag": "td",
-                                "children": [
-                                  {
-                                    "tag": "svg",
-                                    "ns": "http://www.w3.org/2000/svg",
-                                    "children": [
-                                      {
-                                        "tag": "foreignObject",
-                                        "ns": "http://www.w3.org/2000/svg",
-                                        "children": [
-                                          {
-                                            "text": "a"
-                                          }
-                                        ]
-                                      }
-                                    ]
-                                  }
-                                ]
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><table><tbody><tr><td><svg><foreignObject>a</foreignObject></svg></td></tr></tbody></table></body></html>",
-        "noQuirksBodyHtml": "<table><tbody><tr><td><svg><foreignObject>a</foreignObject></svg></td></tr></tbody></table>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><math><mi>a\u0000b",
-      "errors": [
-        "(1,27): invalid-codepoint",
-        "(1,27): invalid-codepoint-in-body",
-        "(1,28): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "math math": true,
-            "math mi": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "math",
-                    "ns": "http://www.w3.org/1998/Math/MathML",
-                    "children": [
-                      {
-                        "tag": "mi",
-                        "ns": "http://www.w3.org/1998/Math/MathML",
-                        "children": [
-                          {
-                            "text": "ab"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><math><mi>ab</mi></math></body></html>",
-        "noQuirksBodyHtml": "<math><mi>ab</mi></math>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><math><mo>a\u0000b",
-      "errors": [
-        "(1,27): invalid-codepoint",
-        "(1,27): invalid-codepoint-in-body",
-        "(1,28): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "math math": true,
-            "math mo": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "math",
-                    "ns": "http://www.w3.org/1998/Math/MathML",
-                    "children": [
-                      {
-                        "tag": "mo",
-                        "ns": "http://www.w3.org/1998/Math/MathML",
-                        "children": [
-                          {
-                            "text": "ab"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><math><mo>ab</mo></math></body></html>",
-        "noQuirksBodyHtml": "<math><mo>ab</mo></math>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><math><mn>a\u0000b",
-      "errors": [
-        "(1,27): invalid-codepoint",
-        "(1,27): invalid-codepoint-in-body",
-        "(1,28): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "math math": true,
-            "math mn": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "math",
-                    "ns": "http://www.w3.org/1998/Math/MathML",
-                    "children": [
-                      {
-                        "tag": "mn",
-                        "ns": "http://www.w3.org/1998/Math/MathML",
-                        "children": [
-                          {
-                            "text": "ab"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><math><mn>ab</mn></math></body></html>",
-        "noQuirksBodyHtml": "<math><mn>ab</mn></math>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><math><ms>a\u0000b",
-      "errors": [
-        "(1,27): invalid-codepoint",
-        "(1,27): invalid-codepoint-in-body",
-        "(1,28): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "math math": true,
-            "math ms": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "math",
-                    "ns": "http://www.w3.org/1998/Math/MathML",
-                    "children": [
-                      {
-                        "tag": "ms",
-                        "ns": "http://www.w3.org/1998/Math/MathML",
-                        "children": [
-                          {
-                            "text": "ab"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><math><ms>ab</ms></math></body></html>",
-        "noQuirksBodyHtml": "<math><ms>ab</ms></math>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><math><mtext>a\u0000b",
-      "errors": [
-        "(1,30): invalid-codepoint",
-        "(1,30): invalid-codepoint-in-body",
-        "(1,31): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "math math": true,
-            "math mtext": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "math",
-                    "ns": "http://www.w3.org/1998/Math/MathML",
-                    "children": [
-                      {
-                        "tag": "mtext",
-                        "ns": "http://www.w3.org/1998/Math/MathML",
-                        "children": [
-                          {
-                            "text": "ab"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><math><mtext>ab</mtext></math></body></html>",
-        "noQuirksBodyHtml": "<math><mtext>ab</mtext></math>"
-      }
-    }
-  ],
-  "ruby.dat": [
-    {
-      "data": "<html><ruby>a<rb>b<rb></ruby></html>",
-      "errors": [
-        "(1,6): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "ruby": true,
-            "rb": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "ruby",
-                    "children": [
-                      {
-                        "text": "a"
-                      },
-                      {
-                        "tag": "rb",
-                        "children": [
-                          {
-                            "text": "b"
-                          }
-                        ]
-                      },
-                      {
-                        "tag": "rb"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><ruby>a<rb>b</rb><rb></rb></ruby></body></html>",
-        "noQuirksBodyHtml": "<ruby>a<rb>b</rb><rb></rb></ruby>"
-      }
-    },
-    {
-      "data": "<html><ruby>a<rb>b<rt></ruby></html>",
-      "errors": [
-        "(1,6): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "ruby": true,
-            "rb": true,
-            "rt": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "ruby",
-                    "children": [
-                      {
-                        "text": "a"
-                      },
-                      {
-                        "tag": "rb",
-                        "children": [
-                          {
-                            "text": "b"
-                          }
-                        ]
-                      },
-                      {
-                        "tag": "rt"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><ruby>a<rb>b</rb><rt></rt></ruby></body></html>",
-        "noQuirksBodyHtml": "<ruby>a<rb>b</rb><rt></rt></ruby>"
-      }
-    },
-    {
-      "data": "<html><ruby>a<rb>b<rtc></ruby></html>",
-      "errors": [
-        "(1,6): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "ruby": true,
-            "rb": true,
-            "rtc": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "ruby",
-                    "children": [
-                      {
-                        "text": "a"
-                      },
-                      {
-                        "tag": "rb",
-                        "children": [
-                          {
-                            "text": "b"
-                          }
-                        ]
-                      },
-                      {
-                        "tag": "rtc"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><ruby>a<rb>b</rb><rtc></rtc></ruby></body></html>",
-        "noQuirksBodyHtml": "<ruby>a<rb>b</rb><rtc></rtc></ruby>"
-      }
-    },
-    {
-      "data": "<html><ruby>a<rb>b<rp></ruby></html>",
-      "errors": [
-        "(1,6): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "ruby": true,
-            "rb": true,
-            "rp": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "ruby",
-                    "children": [
-                      {
-                        "text": "a"
-                      },
-                      {
-                        "tag": "rb",
-                        "children": [
-                          {
-                            "text": "b"
-                          }
-                        ]
-                      },
-                      {
-                        "tag": "rp"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><ruby>a<rb>b</rb><rp></rp></ruby></body></html>",
-        "noQuirksBodyHtml": "<ruby>a<rb>b</rb><rp></rp></ruby>"
-      }
-    },
-    {
-      "data": "<html><ruby>a<rb>b<span></ruby></html>",
-      "errors": [
-        "(1,6): expected-doctype-but-got-start-tag",
-        "(1,31): unexpected-end-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "ruby": true,
-            "rb": true,
-            "span": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "ruby",
-                    "children": [
-                      {
-                        "text": "a"
-                      },
-                      {
-                        "tag": "rb",
-                        "children": [
-                          {
-                            "text": "b"
-                          },
-                          {
-                            "tag": "span"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><ruby>a<rb>b<span></span></rb></ruby></body></html>",
-        "noQuirksBodyHtml": "<ruby>a<rb>b<span></span></rb></ruby>"
-      }
-    },
-    {
-      "data": "<html><ruby>a<rt>b<rb></ruby></html>",
-      "errors": [
-        "(1,6): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "ruby": true,
-            "rt": true,
-            "rb": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "ruby",
-                    "children": [
-                      {
-                        "text": "a"
-                      },
-                      {
-                        "tag": "rt",
-                        "children": [
-                          {
-                            "text": "b"
-                          }
-                        ]
-                      },
-                      {
-                        "tag": "rb"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><ruby>a<rt>b</rt><rb></rb></ruby></body></html>",
-        "noQuirksBodyHtml": "<ruby>a<rt>b</rt><rb></rb></ruby>"
-      }
-    },
-    {
-      "data": "<html><ruby>a<rt>b<rt></ruby></html>",
-      "errors": [
-        "(1,6): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "ruby": true,
-            "rt": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "ruby",
-                    "children": [
-                      {
-                        "text": "a"
-                      },
-                      {
-                        "tag": "rt",
-                        "children": [
-                          {
-                            "text": "b"
-                          }
-                        ]
-                      },
-                      {
-                        "tag": "rt"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><ruby>a<rt>b</rt><rt></rt></ruby></body></html>",
-        "noQuirksBodyHtml": "<ruby>a<rt>b</rt><rt></rt></ruby>"
-      }
-    },
-    {
-      "data": "<html><ruby>a<rt>b<rtc></ruby></html>",
-      "errors": [
-        "(1,6): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "ruby": true,
-            "rt": true,
-            "rtc": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "ruby",
-                    "children": [
-                      {
-                        "text": "a"
-                      },
-                      {
-                        "tag": "rt",
-                        "children": [
-                          {
-                            "text": "b"
-                          }
-                        ]
-                      },
-                      {
-                        "tag": "rtc"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><ruby>a<rt>b</rt><rtc></rtc></ruby></body></html>",
-        "noQuirksBodyHtml": "<ruby>a<rt>b</rt><rtc></rtc></ruby>"
-      }
-    },
-    {
-      "data": "<html><ruby>a<rt>b<rp></ruby></html>",
-      "errors": [
-        "(1,6): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "ruby": true,
-            "rt": true,
-            "rp": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "ruby",
-                    "children": [
-                      {
-                        "text": "a"
-                      },
-                      {
-                        "tag": "rt",
-                        "children": [
-                          {
-                            "text": "b"
-                          }
-                        ]
-                      },
-                      {
-                        "tag": "rp"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><ruby>a<rt>b</rt><rp></rp></ruby></body></html>",
-        "noQuirksBodyHtml": "<ruby>a<rt>b</rt><rp></rp></ruby>"
-      }
-    },
-    {
-      "data": "<html><ruby>a<rt>b<span></ruby></html>",
-      "errors": [
-        "(1,6): expected-doctype-but-got-start-tag",
-        "(1,31): unexpected-end-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "ruby": true,
-            "rt": true,
-            "span": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "ruby",
-                    "children": [
-                      {
-                        "text": "a"
-                      },
-                      {
-                        "tag": "rt",
-                        "children": [
-                          {
-                            "text": "b"
-                          },
-                          {
-                            "tag": "span"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><ruby>a<rt>b<span></span></rt></ruby></body></html>",
-        "noQuirksBodyHtml": "<ruby>a<rt>b<span></span></rt></ruby>"
-      }
-    },
-    {
-      "data": "<html><ruby>a<rtc>b<rb></ruby></html>",
-      "errors": [
-        "(1,6): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "ruby": true,
-            "rtc": true,
-            "rb": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "ruby",
-                    "children": [
-                      {
-                        "text": "a"
-                      },
-                      {
-                        "tag": "rtc",
-                        "children": [
-                          {
-                            "text": "b"
-                          }
-                        ]
-                      },
-                      {
-                        "tag": "rb"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><ruby>a<rtc>b</rtc><rb></rb></ruby></body></html>",
-        "noQuirksBodyHtml": "<ruby>a<rtc>b</rtc><rb></rb></ruby>"
-      }
-    },
-    {
-      "data": "<html><ruby>a<rtc>b<rt>c<rt>d</ruby></html>",
-      "errors": [
-        "(1,6): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "ruby": true,
-            "rtc": true,
-            "rt": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "ruby",
-                    "children": [
-                      {
-                        "text": "a"
-                      },
-                      {
-                        "tag": "rtc",
-                        "children": [
-                          {
-                            "text": "b"
-                          },
-                          {
-                            "tag": "rt",
-                            "children": [
-                              {
-                                "text": "c"
-                              }
-                            ]
-                          },
-                          {
-                            "tag": "rt",
-                            "children": [
-                              {
-                                "text": "d"
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><ruby>a<rtc>b<rt>c</rt><rt>d</rt></rtc></ruby></body></html>",
-        "noQuirksBodyHtml": "<ruby>a<rtc>b<rt>c</rt><rt>d</rt></rtc></ruby>"
-      }
-    },
-    {
-      "data": "<html><ruby>a<rtc>b<rtc></ruby></html>",
-      "errors": [
-        "(1,6): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "ruby": true,
-            "rtc": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "ruby",
-                    "children": [
-                      {
-                        "text": "a"
-                      },
-                      {
-                        "tag": "rtc",
-                        "children": [
-                          {
-                            "text": "b"
-                          }
-                        ]
-                      },
-                      {
-                        "tag": "rtc"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><ruby>a<rtc>b</rtc><rtc></rtc></ruby></body></html>",
-        "noQuirksBodyHtml": "<ruby>a<rtc>b</rtc><rtc></rtc></ruby>"
-      }
-    },
-    {
-      "data": "<html><ruby>a<rtc>b<rp></ruby></html>",
-      "errors": [
-        "(1,6): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "ruby": true,
-            "rtc": true,
-            "rp": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "ruby",
-                    "children": [
-                      {
-                        "text": "a"
-                      },
-                      {
-                        "tag": "rtc",
-                        "children": [
-                          {
-                            "text": "b"
-                          },
-                          {
-                            "tag": "rp"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><ruby>a<rtc>b<rp></rp></rtc></ruby></body></html>",
-        "noQuirksBodyHtml": "<ruby>a<rtc>b<rp></rp></rtc></ruby>"
-      }
-    },
-    {
-      "data": "<html><ruby>a<rtc>b<span></ruby></html>",
-      "errors": [
-        "(1,6): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "ruby": true,
-            "rtc": true,
-            "span": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "ruby",
-                    "children": [
-                      {
-                        "text": "a"
-                      },
-                      {
-                        "tag": "rtc",
-                        "children": [
-                          {
-                            "text": "b"
-                          },
-                          {
-                            "tag": "span"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><ruby>a<rtc>b<span></span></rtc></ruby></body></html>",
-        "noQuirksBodyHtml": "<ruby>a<rtc>b<span></span></rtc></ruby>"
-      }
-    },
-    {
-      "data": "<html><ruby>a<rp>b<rb></ruby></html>",
-      "errors": [
-        "(1,6): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "ruby": true,
-            "rp": true,
-            "rb": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "ruby",
-                    "children": [
-                      {
-                        "text": "a"
-                      },
-                      {
-                        "tag": "rp",
-                        "children": [
-                          {
-                            "text": "b"
-                          }
-                        ]
-                      },
-                      {
-                        "tag": "rb"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><ruby>a<rp>b</rp><rb></rb></ruby></body></html>",
-        "noQuirksBodyHtml": "<ruby>a<rp>b</rp><rb></rb></ruby>"
-      }
-    },
-    {
-      "data": "<html><ruby>a<rp>b<rt></ruby></html>",
-      "errors": [
-        "(1,6): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "ruby": true,
-            "rp": true,
-            "rt": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "ruby",
-                    "children": [
-                      {
-                        "text": "a"
-                      },
-                      {
-                        "tag": "rp",
-                        "children": [
-                          {
-                            "text": "b"
-                          }
-                        ]
-                      },
-                      {
-                        "tag": "rt"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><ruby>a<rp>b</rp><rt></rt></ruby></body></html>",
-        "noQuirksBodyHtml": "<ruby>a<rp>b</rp><rt></rt></ruby>"
-      }
-    },
-    {
-      "data": "<html><ruby>a<rp>b<rtc></ruby></html>",
-      "errors": [
-        "(1,6): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "ruby": true,
-            "rp": true,
-            "rtc": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "ruby",
-                    "children": [
-                      {
-                        "text": "a"
-                      },
-                      {
-                        "tag": "rp",
-                        "children": [
-                          {
-                            "text": "b"
-                          }
-                        ]
-                      },
-                      {
-                        "tag": "rtc"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><ruby>a<rp>b</rp><rtc></rtc></ruby></body></html>",
-        "noQuirksBodyHtml": "<ruby>a<rp>b</rp><rtc></rtc></ruby>"
-      }
-    },
-    {
-      "data": "<html><ruby>a<rp>b<rp></ruby></html>",
-      "errors": [
-        "(1,6): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "ruby": true,
-            "rp": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "ruby",
-                    "children": [
-                      {
-                        "text": "a"
-                      },
-                      {
-                        "tag": "rp",
-                        "children": [
-                          {
-                            "text": "b"
-                          }
-                        ]
-                      },
-                      {
-                        "tag": "rp"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><ruby>a<rp>b</rp><rp></rp></ruby></body></html>",
-        "noQuirksBodyHtml": "<ruby>a<rp>b</rp><rp></rp></ruby>"
-      }
-    },
-    {
-      "data": "<html><ruby>a<rp>b<span></ruby></html>",
-      "errors": [
-        "(1,6): expected-doctype-but-got-start-tag",
-        "(1,31): unexpected-end-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "ruby": true,
-            "rp": true,
-            "span": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "ruby",
-                    "children": [
-                      {
-                        "text": "a"
-                      },
-                      {
-                        "tag": "rp",
-                        "children": [
-                          {
-                            "text": "b"
-                          },
-                          {
-                            "tag": "span"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><ruby>a<rp>b<span></span></rp></ruby></body></html>",
-        "noQuirksBodyHtml": "<ruby>a<rp>b<span></span></rp></ruby>"
-      }
-    },
-    {
-      "data": "<html><ruby><rtc><ruby>a<rb>b<rt></ruby></ruby></html>",
-      "errors": [
-        "(1,6): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "ruby": true,
-            "rtc": true,
-            "rb": true,
-            "rt": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "ruby",
-                    "children": [
-                      {
-                        "tag": "rtc",
-                        "children": [
-                          {
-                            "tag": "ruby",
-                            "children": [
-                              {
-                                "text": "a"
-                              },
-                              {
-                                "tag": "rb",
-                                "children": [
-                                  {
-                                    "text": "b"
-                                  }
-                                ]
-                              },
-                              {
-                                "tag": "rt"
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><ruby><rtc><ruby>a<rb>b</rb><rt></rt></ruby></rtc></ruby></body></html>",
-        "noQuirksBodyHtml": "<ruby><rtc><ruby>a<rb>b</rb><rt></rt></ruby></rtc></ruby>"
-      }
-    }
-  ],
-  "scriptdata01.dat": [
-    {
-      "data": "FOO<script>'Hello'</script>BAR",
-      "errors": [
-        "(1,3): expected-doctype-but-got-chars"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "script": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "FOO"
-                  },
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "'Hello'",
-                        "no_escape": true
-                      }
-                    ]
-                  },
-                  {
-                    "text": "BAR"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>FOO<script>'Hello'</script>BAR</body></html>",
-        "noQuirksBodyHtml": "FOO<script>'Hello'</script>BAR"
-      }
-    },
-    {
-      "data": "FOO<script></script>BAR",
-      "errors": [
-        "(1,3): expected-doctype-but-got-chars"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "script": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "FOO"
-                  },
-                  {
-                    "tag": "script"
-                  },
-                  {
-                    "text": "BAR"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>FOO<script></script>BAR</body></html>",
-        "noQuirksBodyHtml": "FOO<script></script>BAR"
-      }
-    },
-    {
-      "data": "FOO<script></script >BAR",
-      "errors": [
-        "(1,3): expected-doctype-but-got-chars"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "script": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "FOO"
-                  },
-                  {
-                    "tag": "script"
-                  },
-                  {
-                    "text": "BAR"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>FOO<script></script>BAR</body></html>",
-        "noQuirksBodyHtml": "FOO<script></script>BAR"
-      }
-    },
-    {
-      "data": "FOO<script></script/>BAR",
-      "errors": [
-        "(1,3): expected-doctype-but-got-chars",
-        "(1,21): self-closing-flag-on-end-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "script": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "FOO"
-                  },
-                  {
-                    "tag": "script"
-                  },
-                  {
-                    "text": "BAR"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>FOO<script></script>BAR</body></html>",
-        "noQuirksBodyHtml": "FOO<script></script>BAR"
-      }
-    },
-    {
-      "data": "FOO<script></script/ >BAR",
-      "errors": [
-        "(1,3): expected-doctype-but-got-chars",
-        "(1,20): unexpected-character-after-solidus-in-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "script": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "FOO"
-                  },
-                  {
-                    "tag": "script"
-                  },
-                  {
-                    "text": "BAR"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>FOO<script></script>BAR</body></html>",
-        "noQuirksBodyHtml": "FOO<script></script>BAR"
-      }
-    },
-    {
-      "data": "FOO<script type=\"text/plain\"></scriptx>BAR",
-      "errors": [
-        "(1,3): expected-doctype-but-got-chars",
-        "(1,42): expected-named-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "script": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "FOO"
-                  },
-                  {
-                    "tag": "script",
-                    "attrs": [
-                      {
-                        "name": "type",
-                        "value": "text/plain"
-                      }
-                    ],
-                    "children": [
-                      {
-                        "text": "</scriptx>BAR",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>FOO<script type=\"text/plain\"></scriptx>BAR</script></body></html>",
-        "noQuirksBodyHtml": "FOO<script type=\"text/plain\"></scriptx>BAR</script>"
-      }
-    },
-    {
-      "data": "FOO<script></script foo=\">\" dd>BAR",
-      "errors": [
-        "(1,3): expected-doctype-but-got-chars",
-        "(1,31): attributes-in-end-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "script": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "FOO"
-                  },
-                  {
-                    "tag": "script"
-                  },
-                  {
-                    "text": "BAR"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>FOO<script></script>BAR</body></html>",
-        "noQuirksBodyHtml": "FOO<script></script>BAR"
-      }
-    },
-    {
-      "data": "FOO<script>'<'</script>BAR",
-      "errors": [
-        "(1,3): expected-doctype-but-got-chars"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "script": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "FOO"
-                  },
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "'<'",
-                        "no_escape": true
-                      }
-                    ]
-                  },
-                  {
-                    "text": "BAR"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>FOO<script>'<'</script>BAR</body></html>",
-        "noQuirksBodyHtml": "FOO<script>'<'</script>BAR"
-      }
-    },
-    {
-      "data": "FOO<script>'<!'</script>BAR",
-      "errors": [
-        "(1,3): expected-doctype-but-got-chars"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "script": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "FOO"
-                  },
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "'<!'",
-                        "no_escape": true
-                      }
-                    ]
-                  },
-                  {
-                    "text": "BAR"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>FOO<script>'<!'</script>BAR</body></html>",
-        "noQuirksBodyHtml": "FOO<script>'<!'</script>BAR"
-      }
-    },
-    {
-      "data": "FOO<script>'<!-'</script>BAR",
-      "errors": [
-        "(1,3): expected-doctype-but-got-chars"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "script": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "FOO"
-                  },
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "'<!-'",
-                        "no_escape": true
-                      }
-                    ]
-                  },
-                  {
-                    "text": "BAR"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>FOO<script>'<!-'</script>BAR</body></html>",
-        "noQuirksBodyHtml": "FOO<script>'<!-'</script>BAR"
-      }
-    },
-    {
-      "data": "FOO<script>'<!--'</script>BAR",
-      "errors": [
-        "(1,3): expected-doctype-but-got-chars"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "script": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "FOO"
-                  },
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "'<!--'",
-                        "no_escape": true
-                      }
-                    ]
-                  },
-                  {
-                    "text": "BAR"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>FOO<script>'<!--'</script>BAR</body></html>",
-        "noQuirksBodyHtml": "FOO<script>'<!--'</script>BAR"
-      }
-    },
-    {
-      "data": "FOO<script>'<!---'</script>BAR",
-      "errors": [
-        "(1,3): expected-doctype-but-got-chars"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "script": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "FOO"
-                  },
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "'<!---'",
-                        "no_escape": true
-                      }
-                    ]
-                  },
-                  {
-                    "text": "BAR"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>FOO<script>'<!---'</script>BAR</body></html>",
-        "noQuirksBodyHtml": "FOO<script>'<!---'</script>BAR"
-      }
-    },
-    {
-      "data": "FOO<script>'<!-->'</script>BAR",
-      "errors": [
-        "(1,3): expected-doctype-but-got-chars"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "script": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "FOO"
-                  },
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "'<!-->'",
-                        "no_escape": true
-                      }
-                    ]
-                  },
-                  {
-                    "text": "BAR"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>FOO<script>'<!-->'</script>BAR</body></html>",
-        "noQuirksBodyHtml": "FOO<script>'<!-->'</script>BAR"
-      }
-    },
-    {
-      "data": "FOO<script>'<!-->'</script>BAR",
-      "errors": [
-        "(1,3): expected-doctype-but-got-chars"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "script": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "FOO"
-                  },
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "'<!-->'",
-                        "no_escape": true
-                      }
-                    ]
-                  },
-                  {
-                    "text": "BAR"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>FOO<script>'<!-->'</script>BAR</body></html>",
-        "noQuirksBodyHtml": "FOO<script>'<!-->'</script>BAR"
-      }
-    },
-    {
-      "data": "FOO<script>'<!-- potato'</script>BAR",
-      "errors": [
-        "(1,3): expected-doctype-but-got-chars"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "script": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "FOO"
-                  },
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "'<!-- potato'",
-                        "no_escape": true
-                      }
-                    ]
-                  },
-                  {
-                    "text": "BAR"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>FOO<script>'<!-- potato'</script>BAR</body></html>",
-        "noQuirksBodyHtml": "FOO<script>'<!-- potato'</script>BAR"
-      }
-    },
-    {
-      "data": "FOO<script>'<!-- <sCrIpt'</script>BAR",
-      "errors": [
-        "(1,3): expected-doctype-but-got-chars"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "script": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "FOO"
-                  },
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "'<!-- <sCrIpt'",
-                        "no_escape": true
-                      }
-                    ]
-                  },
-                  {
-                    "text": "BAR"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>FOO<script>'<!-- <sCrIpt'</script>BAR</body></html>",
-        "noQuirksBodyHtml": "FOO<script>'<!-- <sCrIpt'</script>BAR"
-      }
-    },
-    {
-      "data": "FOO<script type=\"text/plain\">'<!-- <sCrIpt>'</script>BAR",
-      "errors": [
-        "(1,3): expected-doctype-but-got-chars",
-        "(1,56): expected-script-data-but-got-eof",
-        "(1,56): expected-named-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "script": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "FOO"
-                  },
-                  {
-                    "tag": "script",
-                    "attrs": [
-                      {
-                        "name": "type",
-                        "value": "text/plain"
-                      }
-                    ],
-                    "children": [
-                      {
-                        "text": "'<!-- <sCrIpt>'</script>BAR",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>FOO<script type=\"text/plain\">'<!-- <sCrIpt>'</script>BAR</script></body></html>",
-        "noQuirksBodyHtml": "FOO<script type=\"text/plain\">'<!-- <sCrIpt>'</script>BAR</script>"
-      }
-    },
-    {
-      "data": "FOO<script type=\"text/plain\">'<!-- <sCrIpt> -'</script>BAR",
-      "errors": [
-        "(1,3): expected-doctype-but-got-chars",
-        "(1,58): expected-script-data-but-got-eof",
-        "(1,58): expected-named-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "script": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "FOO"
-                  },
-                  {
-                    "tag": "script",
-                    "attrs": [
-                      {
-                        "name": "type",
-                        "value": "text/plain"
-                      }
-                    ],
-                    "children": [
-                      {
-                        "text": "'<!-- <sCrIpt> -'</script>BAR",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>FOO<script type=\"text/plain\">'<!-- <sCrIpt> -'</script>BAR</script></body></html>",
-        "noQuirksBodyHtml": "FOO<script type=\"text/plain\">'<!-- <sCrIpt> -'</script>BAR</script>"
-      }
-    },
-    {
-      "data": "FOO<script type=\"text/plain\">'<!-- <sCrIpt> --'</script>BAR",
-      "errors": [
-        "(1,3): expected-doctype-but-got-chars",
-        "(1,59): expected-script-data-but-got-eof",
-        "(1,59): expected-named-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "script": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "FOO"
-                  },
-                  {
-                    "tag": "script",
-                    "attrs": [
-                      {
-                        "name": "type",
-                        "value": "text/plain"
-                      }
-                    ],
-                    "children": [
-                      {
-                        "text": "'<!-- <sCrIpt> --'</script>BAR",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>FOO<script type=\"text/plain\">'<!-- <sCrIpt> --'</script>BAR</script></body></html>",
-        "noQuirksBodyHtml": "FOO<script type=\"text/plain\">'<!-- <sCrIpt> --'</script>BAR</script>"
-      }
-    },
-    {
-      "data": "FOO<script>'<!-- <sCrIpt> -->'</script>BAR",
-      "errors": [
-        "(1,3): expected-doctype-but-got-chars"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "script": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "FOO"
-                  },
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "'<!-- <sCrIpt> -->'",
-                        "no_escape": true
-                      }
-                    ]
-                  },
-                  {
-                    "text": "BAR"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>FOO<script>'<!-- <sCrIpt> -->'</script>BAR</body></html>",
-        "noQuirksBodyHtml": "FOO<script>'<!-- <sCrIpt> -->'</script>BAR"
-      }
-    },
-    {
-      "data": "FOO<script type=\"text/plain\">'<!-- <sCrIpt> --!>'</script>BAR",
-      "errors": [
-        "(1,3): expected-doctype-but-got-chars",
-        "(1,61): expected-script-data-but-got-eof",
-        "(1,61): expected-named-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "script": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "FOO"
-                  },
-                  {
-                    "tag": "script",
-                    "attrs": [
-                      {
-                        "name": "type",
-                        "value": "text/plain"
-                      }
-                    ],
-                    "children": [
-                      {
-                        "text": "'<!-- <sCrIpt> --!>'</script>BAR",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>FOO<script type=\"text/plain\">'<!-- <sCrIpt> --!>'</script>BAR</script></body></html>",
-        "noQuirksBodyHtml": "FOO<script type=\"text/plain\">'<!-- <sCrIpt> --!>'</script>BAR</script>"
-      }
-    },
-    {
-      "data": "FOO<script type=\"text/plain\">'<!-- <sCrIpt> -- >'</script>BAR",
-      "errors": [
-        "(1,3): expected-doctype-but-got-chars",
-        "(1,61): expected-script-data-but-got-eof",
-        "(1,61): expected-named-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "script": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "FOO"
-                  },
-                  {
-                    "tag": "script",
-                    "attrs": [
-                      {
-                        "name": "type",
-                        "value": "text/plain"
-                      }
-                    ],
-                    "children": [
-                      {
-                        "text": "'<!-- <sCrIpt> -- >'</script>BAR",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>FOO<script type=\"text/plain\">'<!-- <sCrIpt> -- >'</script>BAR</script></body></html>",
-        "noQuirksBodyHtml": "FOO<script type=\"text/plain\">'<!-- <sCrIpt> -- >'</script>BAR</script>"
-      }
-    },
-    {
-      "data": "FOO<script type=\"text/plain\">'<!-- <sCrIpt '</script>BAR",
-      "errors": [
-        "(1,3): expected-doctype-but-got-chars",
-        "(1,56): expected-script-data-but-got-eof",
-        "(1,56): expected-named-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "script": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "FOO"
-                  },
-                  {
-                    "tag": "script",
-                    "attrs": [
-                      {
-                        "name": "type",
-                        "value": "text/plain"
-                      }
-                    ],
-                    "children": [
-                      {
-                        "text": "'<!-- <sCrIpt '</script>BAR",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>FOO<script type=\"text/plain\">'<!-- <sCrIpt '</script>BAR</script></body></html>",
-        "noQuirksBodyHtml": "FOO<script type=\"text/plain\">'<!-- <sCrIpt '</script>BAR</script>"
-      }
-    },
-    {
-      "data": "FOO<script type=\"text/plain\">'<!-- <sCrIpt/'</script>BAR",
-      "errors": [
-        "(1,3): expected-doctype-but-got-chars",
-        "(1,56): expected-script-data-but-got-eof",
-        "(1,56): expected-named-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "script": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "FOO"
-                  },
-                  {
-                    "tag": "script",
-                    "attrs": [
-                      {
-                        "name": "type",
-                        "value": "text/plain"
-                      }
-                    ],
-                    "children": [
-                      {
-                        "text": "'<!-- <sCrIpt/'</script>BAR",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>FOO<script type=\"text/plain\">'<!-- <sCrIpt/'</script>BAR</script></body></html>",
-        "noQuirksBodyHtml": "FOO<script type=\"text/plain\">'<!-- <sCrIpt/'</script>BAR</script>"
-      }
-    },
-    {
-      "data": "FOO<script type=\"text/plain\">'<!-- <sCrIpt\\'</script>BAR",
-      "errors": [
-        "(1,3): expected-doctype-but-got-chars"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "script": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "FOO"
-                  },
-                  {
-                    "tag": "script",
-                    "attrs": [
-                      {
-                        "name": "type",
-                        "value": "text/plain"
-                      }
-                    ],
-                    "children": [
-                      {
-                        "text": "'<!-- <sCrIpt\\'",
-                        "no_escape": true
-                      }
-                    ]
-                  },
-                  {
-                    "text": "BAR"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>FOO<script type=\"text/plain\">'<!-- <sCrIpt\\'</script>BAR</body></html>",
-        "noQuirksBodyHtml": "FOO<script type=\"text/plain\">'<!-- <sCrIpt\\'</script>BAR"
-      }
-    },
-    {
-      "data": "FOO<script type=\"text/plain\">'<!-- <sCrIpt/'</script>BAR</script>QUX",
-      "errors": [
-        "(1,3): expected-doctype-but-got-chars"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "script": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "FOO"
-                  },
-                  {
-                    "tag": "script",
-                    "attrs": [
-                      {
-                        "name": "type",
-                        "value": "text/plain"
-                      }
-                    ],
-                    "children": [
-                      {
-                        "text": "'<!-- <sCrIpt/'</script>BAR",
-                        "no_escape": true
-                      }
-                    ]
-                  },
-                  {
-                    "text": "QUX"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>FOO<script type=\"text/plain\">'<!-- <sCrIpt/'</script>BAR</script>QUX</body></html>",
-        "noQuirksBodyHtml": "FOO<script type=\"text/plain\">'<!-- <sCrIpt/'</script>BAR</script>QUX"
-      }
-    },
-    {
-      "data": "FOO<script><!--<script>-></script>--></script>QUX",
-      "errors": [
-        "(1,3): expected-doctype-but-got-chars"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "script": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "FOO"
-                  },
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "<!--<script>-></script>-->",
-                        "no_escape": true
-                      }
-                    ]
-                  },
-                  {
-                    "text": "QUX"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>FOO<script><!--<script>-></script>--></script>QUX</body></html>",
-        "noQuirksBodyHtml": "FOO<script><!--<script>-></script>--></script>QUX"
-      }
-    }
-  ],
-  "tables01.dat": [
-    {
-      "data": "<table><th>",
-      "errors": [
-        "(1,7): expected-doctype-but-got-start-tag",
-        "(1,11): unexpected-cell-in-table-body",
-        "(1,11): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true,
-            "tbody": true,
-            "tr": true,
-            "th": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "tbody",
-                        "children": [
-                          {
-                            "tag": "tr",
-                            "children": [
-                              {
-                                "tag": "th"
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><table><tbody><tr><th></th></tr></tbody></table></body></html>",
-        "noQuirksBodyHtml": "<table><tbody><tr><th></th></tr></tbody></table>"
-      }
-    },
-    {
-      "data": "<table><td>",
-      "errors": [
-        "(1,7): expected-doctype-but-got-start-tag",
-        "(1,11): unexpected-cell-in-table-body",
-        "(1,11): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true,
-            "tbody": true,
-            "tr": true,
-            "td": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "tbody",
-                        "children": [
-                          {
-                            "tag": "tr",
-                            "children": [
-                              {
-                                "tag": "td"
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><table><tbody><tr><td></td></tr></tbody></table></body></html>",
-        "noQuirksBodyHtml": "<table><tbody><tr><td></td></tr></tbody></table>"
-      }
-    },
-    {
-      "data": "<table><col foo='bar'>",
-      "errors": [
-        "(1,7): expected-doctype-but-got-start-tag",
-        "(1,22): eof-in-table"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true,
-            "colgroup": true,
-            "col": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "colgroup",
-                        "children": [
-                          {
-                            "tag": "col",
-                            "attrs": [
-                              {
-                                "name": "foo",
-                                "value": "bar"
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><table><colgroup><col foo=\"bar\"></colgroup></table></body></html>",
-        "noQuirksBodyHtml": "<table><colgroup><col foo=\"bar\"></colgroup></table>"
-      }
-    },
-    {
-      "data": "<table><colgroup></html>foo",
-      "errors": [
-        "(1,7): expected-doctype-but-got-start-tag",
-        "(1,24): unexpected-end-tag",
-        "(1,27): foster-parenting-character-in-table",
-        "(1,27): foster-parenting-character-in-table",
-        "(1,27): foster-parenting-character-in-table",
-        "(1,27): eof-in-table"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true,
-            "colgroup": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "foo"
-                  },
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "colgroup"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>foo<table><colgroup></colgroup></table></body></html>",
-        "noQuirksBodyHtml": "foo<table><colgroup></colgroup></table>"
-      }
-    },
-    {
-      "data": "<table></table><p>foo",
-      "errors": [
-        "(1,7): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true,
-            "p": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "table"
-                  },
-                  {
-                    "tag": "p",
-                    "children": [
-                      {
-                        "text": "foo"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><table></table><p>foo</p></body></html>",
-        "noQuirksBodyHtml": "<table></table><p>foo</p>"
-      }
-    },
-    {
-      "data": "<table></body></caption></col></colgroup></html></tbody></td></tfoot></th></thead></tr><td>",
-      "errors": [
-        "(1,7): expected-doctype-but-got-start-tag",
-        "(1,14): unexpected-end-tag",
-        "(1,24): unexpected-end-tag",
-        "(1,30): unexpected-end-tag",
-        "(1,41): unexpected-end-tag",
-        "(1,48): unexpected-end-tag",
-        "(1,56): unexpected-end-tag",
-        "(1,61): unexpected-end-tag",
-        "(1,69): unexpected-end-tag",
-        "(1,74): unexpected-end-tag",
-        "(1,82): unexpected-end-tag",
-        "(1,87): unexpected-end-tag",
-        "(1,91): unexpected-cell-in-table-body",
-        "(1,91): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true,
-            "tbody": true,
-            "tr": true,
-            "td": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "tbody",
-                        "children": [
-                          {
-                            "tag": "tr",
-                            "children": [
-                              {
-                                "tag": "td"
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><table><tbody><tr><td></td></tr></tbody></table></body></html>",
-        "noQuirksBodyHtml": "<table><tbody><tr><td></td></tr></tbody></table>"
-      }
-    },
-    {
-      "data": "<table><select><option>3</select></table>",
-      "errors": [
-        "(1,7): expected-doctype-but-got-start-tag",
-        "(1,15): unexpected-start-tag-implies-table-voodoo"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "select": true,
-            "option": true,
-            "table": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "select",
-                    "children": [
-                      {
-                        "tag": "option",
-                        "children": [
-                          {
-                            "text": "3"
-                          }
-                        ]
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "table"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><select><option>3</option></select><table></table></body></html>",
-        "noQuirksBodyHtml": "<select><option>3</option></select><table></table>"
-      }
-    },
-    {
-      "data": "<table><select><table></table></select></table>",
-      "errors": [
-        "(1,7): expected-doctype-but-got-start-tag",
-        "(1,15): unexpected-start-tag-implies-table-voodoo",
-        "(1,22): unexpected-table-element-start-tag-in-select-in-table",
-        "(1,22): unexpected-start-tag-implies-end-tag",
-        "(1,39): unexpected-end-tag",
-        "(1,47): unexpected-end-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "select": true,
-            "table": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "select"
-                  },
-                  {
-                    "tag": "table"
-                  },
-                  {
-                    "tag": "table"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><select></select><table></table><table></table></body></html>",
-        "noQuirksBodyHtml": "<select></select><table></table><table></table>"
-      }
-    },
-    {
-      "data": "<table><select></table>",
-      "errors": [
-        "(1,7): expected-doctype-but-got-start-tag",
-        "(1,15): unexpected-start-tag-implies-table-voodoo",
-        "(1,23): unexpected-table-element-end-tag-in-select-in-table"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "select": true,
-            "table": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "select"
-                  },
-                  {
-                    "tag": "table"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><select></select><table></table></body></html>",
-        "noQuirksBodyHtml": "<select></select><table></table>"
-      }
-    },
-    {
-      "data": "<table><select><option>A<tr><td>B</td></tr></table>",
-      "errors": [
-        "(1,7): expected-doctype-but-got-start-tag",
-        "(1,15): unexpected-start-tag-implies-table-voodoo",
-        "(1,28): unexpected-table-element-start-tag-in-select-in-table"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "select": true,
-            "option": true,
-            "table": true,
-            "tbody": true,
-            "tr": true,
-            "td": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "select",
-                    "children": [
-                      {
-                        "tag": "option",
-                        "children": [
-                          {
-                            "text": "A"
-                          }
-                        ]
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "tbody",
-                        "children": [
-                          {
-                            "tag": "tr",
-                            "children": [
-                              {
-                                "tag": "td",
-                                "children": [
-                                  {
-                                    "text": "B"
-                                  }
-                                ]
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><select><option>A</option></select><table><tbody><tr><td>B</td></tr></tbody></table></body></html>",
-        "noQuirksBodyHtml": "<select><option>A</option></select><table><tbody><tr><td>B</td></tr></tbody></table>"
-      }
-    },
-    {
-      "data": "<table><td></body></caption></col></colgroup></html>foo",
-      "errors": [
-        "(1,7): expected-doctype-but-got-start-tag",
-        "(1,11): unexpected-cell-in-table-body",
-        "(1,18): unexpected-end-tag",
-        "(1,28): unexpected-end-tag",
-        "(1,34): unexpected-end-tag",
-        "(1,45): unexpected-end-tag",
-        "(1,52): unexpected-end-tag",
-        "(1,55): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true,
-            "tbody": true,
-            "tr": true,
-            "td": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "tbody",
-                        "children": [
-                          {
-                            "tag": "tr",
-                            "children": [
-                              {
-                                "tag": "td",
-                                "children": [
-                                  {
-                                    "text": "foo"
-                                  }
-                                ]
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><table><tbody><tr><td>foo</td></tr></tbody></table></body></html>",
-        "noQuirksBodyHtml": "<table><tbody><tr><td>foo</td></tr></tbody></table>"
-      }
-    },
-    {
-      "data": "<table><td>A</table>B",
-      "errors": [
-        "(1,7): expected-doctype-but-got-start-tag",
-        "(1,11): unexpected-cell-in-table-body"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true,
-            "tbody": true,
-            "tr": true,
-            "td": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "tbody",
-                        "children": [
-                          {
-                            "tag": "tr",
-                            "children": [
-                              {
-                                "tag": "td",
-                                "children": [
-                                  {
-                                    "text": "A"
-                                  }
-                                ]
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  },
-                  {
-                    "text": "B"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><table><tbody><tr><td>A</td></tr></tbody></table>B</body></html>",
-        "noQuirksBodyHtml": "<table><tbody><tr><td>A</td></tr></tbody></table>B"
-      }
-    },
-    {
-      "data": "<table><tr><caption>",
-      "errors": [
-        "(1,7): expected-doctype-but-got-start-tag",
-        "(1,20): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true,
-            "tbody": true,
-            "tr": true,
-            "caption": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "tbody",
-                        "children": [
-                          {
-                            "tag": "tr"
-                          }
-                        ]
-                      },
-                      {
-                        "tag": "caption"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><table><tbody><tr></tr></tbody><caption></caption></table></body></html>",
-        "noQuirksBodyHtml": "<table><tbody><tr></tr></tbody><caption></caption></table>"
-      }
-    },
-    {
-      "data": "<table><tr></body></caption></col></colgroup></html></td></th><td>foo",
-      "errors": [
-        "(1,7): expected-doctype-but-got-start-tag",
-        "(1,18): unexpected-end-tag-in-table-row",
-        "(1,28): unexpected-end-tag-in-table-row",
-        "(1,34): unexpected-end-tag-in-table-row",
-        "(1,45): unexpected-end-tag-in-table-row",
-        "(1,52): unexpected-end-tag-in-table-row",
-        "(1,57): unexpected-end-tag-in-table-row",
-        "(1,62): unexpected-end-tag-in-table-row",
-        "(1,69): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true,
-            "tbody": true,
-            "tr": true,
-            "td": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "tbody",
-                        "children": [
-                          {
-                            "tag": "tr",
-                            "children": [
-                              {
-                                "tag": "td",
-                                "children": [
-                                  {
-                                    "text": "foo"
-                                  }
-                                ]
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><table><tbody><tr><td>foo</td></tr></tbody></table></body></html>",
-        "noQuirksBodyHtml": "<table><tbody><tr><td>foo</td></tr></tbody></table>"
-      }
-    },
-    {
-      "data": "<table><td><tr>",
-      "errors": [
-        "(1,7): expected-doctype-but-got-start-tag",
-        "(1,11): unexpected-cell-in-table-body",
-        "(1,15): eof-in-table"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true,
-            "tbody": true,
-            "tr": true,
-            "td": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "tbody",
-                        "children": [
-                          {
-                            "tag": "tr",
-                            "children": [
-                              {
-                                "tag": "td"
-                              }
-                            ]
-                          },
-                          {
-                            "tag": "tr"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><table><tbody><tr><td></td></tr><tr></tr></tbody></table></body></html>",
-        "noQuirksBodyHtml": "<table><tbody><tr><td></td></tr><tr></tr></tbody></table>"
-      }
-    },
-    {
-      "data": "<table><td><button><td>",
-      "errors": [
-        "(1,7): expected-doctype-but-got-start-tag",
-        "(1,11): unexpected-cell-in-table-body",
-        "(1,23): unexpected-cell-end-tag",
-        "(1,23): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true,
-            "tbody": true,
-            "tr": true,
-            "td": true,
-            "button": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "tbody",
-                        "children": [
-                          {
-                            "tag": "tr",
-                            "children": [
-                              {
-                                "tag": "td",
-                                "children": [
-                                  {
-                                    "tag": "button"
-                                  }
-                                ]
-                              },
-                              {
-                                "tag": "td"
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><table><tbody><tr><td><button></button></td><td></td></tr></tbody></table></body></html>",
-        "noQuirksBodyHtml": "<table><tbody><tr><td><button></button></td><td></td></tr></tbody></table>"
-      }
-    },
-    {
-      "data": "<table><tr><td><svg><desc><td>",
-      "errors": [
-        "(1,7): expected-doctype-but-got-start-tag",
-        "(1,30): unexpected-cell-end-tag",
-        "(1,30): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true,
-            "tbody": true,
-            "tr": true,
-            "td": true,
-            "svg svg": true,
-            "svg desc": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "tbody",
-                        "children": [
-                          {
-                            "tag": "tr",
-                            "children": [
-                              {
-                                "tag": "td",
-                                "children": [
-                                  {
-                                    "tag": "svg",
-                                    "ns": "http://www.w3.org/2000/svg",
-                                    "children": [
-                                      {
-                                        "tag": "desc",
-                                        "ns": "http://www.w3.org/2000/svg"
-                                      }
-                                    ]
-                                  }
-                                ]
-                              },
-                              {
-                                "tag": "td"
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><table><tbody><tr><td><svg><desc></desc></svg></td><td></td></tr></tbody></table></body></html>",
-        "noQuirksBodyHtml": "<table><tbody><tr><td><svg><desc></desc></svg></td><td></td></tr></tbody></table>"
-      }
-    }
-  ],
-  "template.dat": [
-    {
-      "data": "<body><template>Hello</template>",
-      "errors": [
-        "no doctype"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "template": true
-          },
-          "template": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "template",
-                    "children": [
-                      {
-                        "content": true,
-                        "children": [
-                          {
-                            "text": "Hello"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><template>Hello</template></body></html>",
-        "noQuirksBodyHtml": "<template>Hello</template>"
-      }
-    },
-    {
-      "data": "<template>Hello</template>",
-      "errors": [
-        "no doctype"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "template": true,
-            "body": true
-          },
-          "template": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "template",
-                    "children": [
-                      {
-                        "content": true,
-                        "children": [
-                          {
-                            "text": "Hello"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><template>Hello</template></head><body></body></html>",
-        "noQuirksBodyHtml": "<template>Hello</template>"
-      }
-    },
-    {
-      "data": "<template></template><div></div>",
-      "errors": [
-        "no doctype"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "template": true,
-            "body": true,
-            "div": true
-          },
-          "template": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "template",
-                    "children": [
-                      {
-                        "content": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "div"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><template></template></head><body><div></div></body></html>",
-        "noQuirksBodyHtml": "<template></template><div></div>"
-      }
-    },
-    {
-      "data": "<html><template>Hello</template>",
-      "errors": [
-        "no doctype"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "template": true,
-            "body": true
-          },
-          "template": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "template",
-                    "children": [
-                      {
-                        "content": true,
-                        "children": [
-                          {
-                            "text": "Hello"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><template>Hello</template></head><body></body></html>",
-        "noQuirksBodyHtml": "<template>Hello</template>"
-      }
-    },
-    {
-      "data": "<head><template><div></div></template></head>",
-      "errors": [
-        "no doctype"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "template": true,
-            "div": true,
-            "body": true
-          },
-          "template": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "template",
-                    "children": [
-                      {
-                        "content": true,
-                        "children": [
-                          {
-                            "tag": "div"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><template><div></div></template></head><body></body></html>",
-        "noQuirksBodyHtml": "<template><div></div></template>"
-      }
-    },
-    {
-      "data": "<div><template><div><span></template><b>",
-      "errors": [
-        " * (1,6) missing DOCTYPE",
-        " * (1,38) mismatched template end tag",
-        " * (1,41) unexpected end of file"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "div": true,
-            "template": true,
-            "span": true,
-            "b": true
-          },
-          "template": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "div",
-                    "children": [
-                      {
-                        "tag": "template",
-                        "children": [
-                          {
-                            "content": true,
-                            "children": [
-                              {
-                                "tag": "div",
-                                "children": [
-                                  {
-                                    "tag": "span"
-                                  }
-                                ]
-                              }
-                            ]
-                          }
-                        ]
-                      },
-                      {
-                        "tag": "b"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><div><template><div><span></span></div></template><b></b></div></body></html>",
-        "noQuirksBodyHtml": "<div><template><div><span></span></div></template><b></b></div>"
-      }
-    },
-    {
-      "data": "<div><template></div>Hello",
-      "errors": [
-        " * (1,6) missing DOCTYPE",
-        " * (1,22) unexpected token in template",
-        " * (1,27) unexpected end of file in template",
-        " * (1,27) unexpected end of file"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "div": true,
-            "template": true
-          },
-          "template": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "div",
-                    "children": [
-                      {
-                        "tag": "template",
-                        "children": [
-                          {
-                            "content": true,
-                            "children": [
-                              {
-                                "text": "Hello"
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><div><template>Hello</template></div></body></html>",
-        "noQuirksBodyHtml": "<div><template>Hello</template></div>"
-      }
-    },
-    {
-      "data": "<div></template></div>",
-      "errors": [
-        " * (1,6) missing DOCTYPE",
-        " * (1,17) unexpected template end tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "div": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "div"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><div></div></body></html>",
-        "noQuirksBodyHtml": "<div></div>"
-      }
-    },
-    {
-      "data": "<table><template></template></table>",
-      "errors": [
-        "no doctype"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true,
-            "template": true
-          },
-          "template": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "template",
-                        "children": [
-                          {
-                            "content": true
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><table><template></template></table></body></html>",
-        "noQuirksBodyHtml": "<table><template></template></table>"
-      }
-    },
-    {
-      "data": "<table><template></template></div>",
-      "errors": [
-        " * (1,8) missing DOCTYPE",
-        " * (1,35) unexpected token in table - foster parenting",
-        " * (1,35) unexpected end tag",
-        " * (1,35) unexpected end of file"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true,
-            "template": true
-          },
-          "template": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "template",
-                        "children": [
-                          {
-                            "content": true
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><table><template></template></table></body></html>",
-        "noQuirksBodyHtml": "<table><template></template></table>"
-      }
-    },
-    {
-      "data": "<table><div><template></template></div>",
-      "errors": [
-        " * (1,8) missing DOCTYPE",
-        " * (1,13) unexpected token in table - foster parenting",
-        " * (1,40) unexpected token in table - foster parenting",
-        " * (1,40) unexpected end of file"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "div": true,
-            "template": true,
-            "table": true
-          },
-          "template": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "div",
-                    "children": [
-                      {
-                        "tag": "template",
-                        "children": [
-                          {
-                            "content": true
-                          }
-                        ]
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "table"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><div><template></template></div><table></table></body></html>",
-        "noQuirksBodyHtml": "<div><template></template></div><table></table>"
-      }
-    },
-    {
-      "data": "<table><template></template><div></div>",
-      "errors": [
-        "no doctype",
-        "bad div in table",
-        "bad /div in table",
-        "eof in table"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "div": true,
-            "table": true,
-            "template": true
-          },
-          "template": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "div"
-                  },
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "template",
-                        "children": [
-                          {
-                            "content": true
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><div></div><table><template></template></table></body></html>",
-        "noQuirksBodyHtml": "<div></div><table><template></template></table>"
-      }
-    },
-    {
-      "data": "<table>   <template></template></table>",
-      "errors": [
-        "no doctype"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true,
-            "template": true
-          },
-          "template": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "text": "   "
-                      },
-                      {
-                        "tag": "template",
-                        "children": [
-                          {
-                            "content": true
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><table>   <template></template></table></body></html>",
-        "noQuirksBodyHtml": "<table>   <template></template></table>"
-      }
-    },
-    {
-      "data": "<table><tbody><template></template></tbody>",
-      "errors": [
-        "no doctype",
-        "eof in table"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true,
-            "tbody": true,
-            "template": true
-          },
-          "template": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "tbody",
-                        "children": [
-                          {
-                            "tag": "template",
-                            "children": [
-                              {
-                                "content": true
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><table><tbody><template></template></tbody></table></body></html>",
-        "noQuirksBodyHtml": "<table><tbody><template></template></tbody></table>"
-      }
-    },
-    {
-      "data": "<table><tbody><template></tbody></template>",
-      "errors": [
-        "no doctype",
-        "bad /tbody",
-        "eof in table"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true,
-            "tbody": true,
-            "template": true
-          },
-          "template": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "tbody",
-                        "children": [
-                          {
-                            "tag": "template",
-                            "children": [
-                              {
-                                "content": true
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><table><tbody><template></template></tbody></table></body></html>",
-        "noQuirksBodyHtml": "<table><tbody><template></template></tbody></table>"
-      }
-    },
-    {
-      "data": "<table><tbody><template></template></tbody></table>",
-      "errors": [
-        "no doctype"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true,
-            "tbody": true,
-            "template": true
-          },
-          "template": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "tbody",
-                        "children": [
-                          {
-                            "tag": "template",
-                            "children": [
-                              {
-                                "content": true
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><table><tbody><template></template></tbody></table></body></html>",
-        "noQuirksBodyHtml": "<table><tbody><template></template></tbody></table>"
-      }
-    },
-    {
-      "data": "<table><thead><template></template></thead>",
-      "errors": [
-        "no doctype",
-        "eof in table"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true,
-            "thead": true,
-            "template": true
-          },
-          "template": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "thead",
-                        "children": [
-                          {
-                            "tag": "template",
-                            "children": [
-                              {
-                                "content": true
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><table><thead><template></template></thead></table></body></html>",
-        "noQuirksBodyHtml": "<table><thead><template></template></thead></table>"
-      }
-    },
-    {
-      "data": "<table><tfoot><template></template></tfoot>",
-      "errors": [
-        "no doctype",
-        "eof in table"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true,
-            "tfoot": true,
-            "template": true
-          },
-          "template": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "tfoot",
-                        "children": [
-                          {
-                            "tag": "template",
-                            "children": [
-                              {
-                                "content": true
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><table><tfoot><template></template></tfoot></table></body></html>",
-        "noQuirksBodyHtml": "<table><tfoot><template></template></tfoot></table>"
-      }
-    },
-    {
-      "data": "<select><template></template></select>",
-      "errors": [
-        "no doctype"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "select": true,
-            "template": true
-          },
-          "template": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "select",
-                    "children": [
-                      {
-                        "tag": "template",
-                        "children": [
-                          {
-                            "content": true
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><select><template></template></select></body></html>",
-        "noQuirksBodyHtml": "<select><template></template></select>"
-      }
-    },
-    {
-      "data": "<select><template><option></option></template></select>",
-      "errors": [
-        "no doctype"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "select": true,
-            "template": true,
-            "option": true
-          },
-          "template": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "select",
-                    "children": [
-                      {
-                        "tag": "template",
-                        "children": [
-                          {
-                            "content": true,
-                            "children": [
-                              {
-                                "tag": "option"
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><select><template><option></option></template></select></body></html>",
-        "noQuirksBodyHtml": "<select><template><option></option></template></select>"
-      }
-    },
-    {
-      "data": "<template><option></option></select><option></option></template>",
-      "errors": [
-        "no doctype",
-        "bad /select"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "template": true,
-            "option": true,
-            "body": true
-          },
-          "template": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "template",
-                    "children": [
-                      {
-                        "content": true,
-                        "children": [
-                          {
-                            "tag": "option"
-                          },
-                          {
-                            "tag": "option"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><template><option></option><option></option></template></head><body></body></html>",
-        "noQuirksBodyHtml": "<template><option></option><option></option></template>"
-      }
-    },
-    {
-      "data": "<select><template></template><option></select>",
-      "errors": [
-        "no doctype"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "select": true,
-            "template": true,
-            "option": true
-          },
-          "template": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "select",
-                    "children": [
-                      {
-                        "tag": "template",
-                        "children": [
-                          {
-                            "content": true
-                          }
-                        ]
-                      },
-                      {
-                        "tag": "option"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><select><template></template><option></option></select></body></html>",
-        "noQuirksBodyHtml": "<select><template></template><option></option></select>"
-      }
-    },
-    {
-      "data": "<select><option><template></template></select>",
-      "errors": [
-        "no doctype"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "select": true,
-            "option": true,
-            "template": true
-          },
-          "template": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "select",
-                    "children": [
-                      {
-                        "tag": "option",
-                        "children": [
-                          {
-                            "tag": "template",
-                            "children": [
-                              {
-                                "content": true
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><select><option><template></template></option></select></body></html>",
-        "noQuirksBodyHtml": "<select><option><template></template></option></select>"
-      }
-    },
-    {
-      "data": "<select><template>",
-      "errors": [
-        "no doctype",
-        "eof in template",
-        "eof in select"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "select": true,
-            "template": true
-          },
-          "template": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "select",
-                    "children": [
-                      {
-                        "tag": "template",
-                        "children": [
-                          {
-                            "content": true
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><select><template></template></select></body></html>",
-        "noQuirksBodyHtml": "<select><template></template></select>"
-      }
-    },
-    {
-      "data": "<select><option></option><template>",
-      "errors": [
-        "no doctype",
-        "eof in template",
-        "eof in select"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "select": true,
-            "option": true,
-            "template": true
-          },
-          "template": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "select",
-                    "children": [
-                      {
-                        "tag": "option"
-                      },
-                      {
-                        "tag": "template",
-                        "children": [
-                          {
-                            "content": true
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><select><option></option><template></template></select></body></html>",
-        "noQuirksBodyHtml": "<select><option></option><template></template></select>"
-      }
-    },
-    {
-      "data": "<select><option></option><template><option>",
-      "errors": [
-        "no doctype",
-        "eof in template",
-        "eof in select"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "select": true,
-            "option": true,
-            "template": true
-          },
-          "template": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "select",
-                    "children": [
-                      {
-                        "tag": "option"
-                      },
-                      {
-                        "tag": "template",
-                        "children": [
-                          {
-                            "content": true,
-                            "children": [
-                              {
-                                "tag": "option"
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><select><option></option><template><option></option></template></select></body></html>",
-        "noQuirksBodyHtml": "<select><option></option><template><option></option></template></select>"
-      }
-    },
-    {
-      "data": "<table><thead><template><td></template></table>",
-      "errors": [
-        " * (1,8) missing DOCTYPE"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true,
-            "thead": true,
-            "template": true,
-            "td": true
-          },
-          "template": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "thead",
-                        "children": [
-                          {
-                            "tag": "template",
-                            "children": [
-                              {
-                                "content": true,
-                                "children": [
-                                  {
-                                    "tag": "td"
-                                  }
-                                ]
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><table><thead><template><td></td></template></thead></table></body></html>",
-        "noQuirksBodyHtml": "<table><thead><template><td></td></template></thead></table>"
-      }
-    },
-    {
-      "data": "<table><template><thead></template></table>",
-      "errors": [
-        "no doctype"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true,
-            "template": true,
-            "thead": true
-          },
-          "template": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "template",
-                        "children": [
-                          {
-                            "content": true,
-                            "children": [
-                              {
-                                "tag": "thead"
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><table><template><thead></thead></template></table></body></html>",
-        "noQuirksBodyHtml": "<table><template><thead></thead></template></table>"
-      }
-    },
-    {
-      "data": "<body><table><template><td></tr><div></template></table>",
-      "errors": [
-        "no doctype",
-        "bad </tr>",
-        "missing </div>"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true,
-            "template": true,
-            "td": true,
-            "div": true
-          },
-          "template": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "template",
-                        "children": [
-                          {
-                            "content": true,
-                            "children": [
-                              {
-                                "tag": "td",
-                                "children": [
-                                  {
-                                    "tag": "div"
-                                  }
-                                ]
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><table><template><td><div></div></td></template></table></body></html>",
-        "noQuirksBodyHtml": "<table><template><td><div></div></td></template></table>"
-      }
-    },
-    {
-      "data": "<table><template><thead></template></thead></table>",
-      "errors": [
-        "no doctype",
-        "bad /thead after /template"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true,
-            "template": true,
-            "thead": true
-          },
-          "template": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "template",
-                        "children": [
-                          {
-                            "content": true,
-                            "children": [
-                              {
-                                "tag": "thead"
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><table><template><thead></thead></template></table></body></html>",
-        "noQuirksBodyHtml": "<table><template><thead></thead></template></table>"
-      }
-    },
-    {
-      "data": "<table><thead><template><tr></template></table>",
-      "errors": [
-        "no doctype"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true,
-            "thead": true,
-            "template": true,
-            "tr": true
-          },
-          "template": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "thead",
-                        "children": [
-                          {
-                            "tag": "template",
-                            "children": [
-                              {
-                                "content": true,
-                                "children": [
-                                  {
-                                    "tag": "tr"
-                                  }
-                                ]
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><table><thead><template><tr></tr></template></thead></table></body></html>",
-        "noQuirksBodyHtml": "<table><thead><template><tr></tr></template></thead></table>"
-      }
-    },
-    {
-      "data": "<table><template><tr></template></table>",
-      "errors": [
-        "no doctype"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true,
-            "template": true,
-            "tr": true
-          },
-          "template": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "template",
-                        "children": [
-                          {
-                            "content": true,
-                            "children": [
-                              {
-                                "tag": "tr"
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><table><template><tr></tr></template></table></body></html>",
-        "noQuirksBodyHtml": "<table><template><tr></tr></template></table>"
-      }
-    },
-    {
-      "data": "<table><tr><template><td>",
-      "errors": [
-        "no doctype",
-        "eof in template",
-        "eof in table"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true,
-            "tbody": true,
-            "tr": true,
-            "template": true,
-            "td": true
-          },
-          "template": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "tbody",
-                        "children": [
-                          {
-                            "tag": "tr",
-                            "children": [
-                              {
-                                "tag": "template",
-                                "children": [
-                                  {
-                                    "content": true,
-                                    "children": [
-                                      {
-                                        "tag": "td"
-                                      }
-                                    ]
-                                  }
-                                ]
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><table><tbody><tr><template><td></td></template></tr></tbody></table></body></html>",
-        "noQuirksBodyHtml": "<table><tbody><tr><template><td></td></template></tr></tbody></table>"
-      }
-    },
-    {
-      "data": "<table><template><tr><template><td></template></tr></template></table>",
-      "errors": [
-        "no doctype"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true,
-            "template": true,
-            "tr": true,
-            "td": true
-          },
-          "template": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "template",
-                        "children": [
-                          {
-                            "content": true,
-                            "children": [
-                              {
-                                "tag": "tr",
-                                "children": [
-                                  {
-                                    "tag": "template",
-                                    "children": [
-                                      {
-                                        "content": true,
-                                        "children": [
-                                          {
-                                            "tag": "td"
-                                          }
-                                        ]
-                                      }
-                                    ]
-                                  }
-                                ]
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><table><template><tr><template><td></td></template></tr></template></table></body></html>",
-        "noQuirksBodyHtml": "<table><template><tr><template><td></td></template></tr></template></table>"
-      }
-    },
-    {
-      "data": "<table><template><tr><template><td></td></template></tr></template></table>",
-      "errors": [
-        "no doctype"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true,
-            "template": true,
-            "tr": true,
-            "td": true
-          },
-          "template": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "template",
-                        "children": [
-                          {
-                            "content": true,
-                            "children": [
-                              {
-                                "tag": "tr",
-                                "children": [
-                                  {
-                                    "tag": "template",
-                                    "children": [
-                                      {
-                                        "content": true,
-                                        "children": [
-                                          {
-                                            "tag": "td"
-                                          }
-                                        ]
-                                      }
-                                    ]
-                                  }
-                                ]
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><table><template><tr><template><td></td></template></tr></template></table></body></html>",
-        "noQuirksBodyHtml": "<table><template><tr><template><td></td></template></tr></template></table>"
-      }
-    },
-    {
-      "data": "<table><template><td></template>",
-      "errors": [
-        "no doctype",
-        "eof in table"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true,
-            "template": true,
-            "td": true
-          },
-          "template": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "template",
-                        "children": [
-                          {
-                            "content": true,
-                            "children": [
-                              {
-                                "tag": "td"
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><table><template><td></td></template></table></body></html>",
-        "noQuirksBodyHtml": "<table><template><td></td></template></table>"
-      }
-    },
-    {
-      "data": "<body><template><td></td></template>",
-      "errors": [
-        "no doctype"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "template": true,
-            "td": true
-          },
-          "template": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "template",
-                    "children": [
-                      {
-                        "content": true,
-                        "children": [
-                          {
-                            "tag": "td"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><template><td></td></template></body></html>",
-        "noQuirksBodyHtml": "<template><td></td></template>"
-      }
-    },
-    {
-      "data": "<body><template><template><tr></tr></template><td></td></template>",
-      "errors": [
-        "no doctype"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "template": true,
-            "tr": true,
-            "td": true
-          },
-          "template": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "template",
-                    "children": [
-                      {
-                        "content": true,
-                        "children": [
-                          {
-                            "tag": "template",
-                            "children": [
-                              {
-                                "content": true,
-                                "children": [
-                                  {
-                                    "tag": "tr"
-                                  }
-                                ]
-                              }
-                            ]
-                          },
-                          {
-                            "tag": "td"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><template><template><tr></tr></template><td></td></template></body></html>",
-        "noQuirksBodyHtml": "<template><template><tr></tr></template><td></td></template>"
-      }
-    },
-    {
-      "data": "<table><colgroup><template><col>",
-      "errors": [
-        "no doctype",
-        "eof in template",
-        "eof in table"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true,
-            "colgroup": true,
-            "template": true,
-            "col": true
-          },
-          "template": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "colgroup",
-                        "children": [
-                          {
-                            "tag": "template",
-                            "children": [
-                              {
-                                "content": true,
-                                "children": [
-                                  {
-                                    "tag": "col"
-                                  }
-                                ]
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><table><colgroup><template><col></template></colgroup></table></body></html>",
-        "noQuirksBodyHtml": "<table><colgroup><template><col></template></colgroup></table>"
-      }
-    },
-    {
-      "data": "<frameset><template><frame></frame></template></frameset>",
-      "errors": [
-        " * (1,11) missing DOCTYPE",
-        " * (1,21) unexpected start tag token",
-        " * (1,36) unexpected end tag token",
-        " * (1,47) unexpected end tag token"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "frameset": true,
-            "frame": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "frameset",
-                "children": [
-                  {
-                    "tag": "frame"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><frameset><frame></frameset></html>",
-        "noQuirksBodyHtml": "<template></template>"
-      }
-    },
-    {
-      "data": "<template><frame></frame></frameset><frame></frame></template>",
-      "errors": [
-        " * (1,11) missing DOCTYPE",
-        " * (1,18) unexpected start tag",
-        " * (1,26) unexpected end tag",
-        " * (1,37) unexpected end tag",
-        " * (1,44) unexpected start tag",
-        " * (1,52) unexpected end tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "template": true,
-            "body": true
-          },
-          "template": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "template",
-                    "children": [
-                      {
-                        "content": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><template></template></head><body></body></html>",
-        "noQuirksBodyHtml": "<template></template>"
-      }
-    },
-    {
-      "data": "<template><div><frameset><span></span></div><span></span></template>",
-      "errors": [
-        "no doctype",
-        "bad frameset"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "template": true,
-            "div": true,
-            "span": true,
-            "body": true
-          },
-          "template": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "template",
-                    "children": [
-                      {
-                        "content": true,
-                        "children": [
-                          {
-                            "tag": "div",
-                            "children": [
-                              {
-                                "tag": "span"
-                              }
-                            ]
-                          },
-                          {
-                            "tag": "span"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><template><div><span></span></div><span></span></template></head><body></body></html>",
-        "noQuirksBodyHtml": "<template><div><span></span></div><span></span></template>"
-      }
-    },
-    {
-      "data": "<body><template><div><frameset><span></span></div><span></span></template></body>",
-      "errors": [
-        "no doctype",
-        "bad frameset"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "template": true,
-            "div": true,
-            "span": true
-          },
-          "template": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "template",
-                    "children": [
-                      {
-                        "content": true,
-                        "children": [
-                          {
-                            "tag": "div",
-                            "children": [
-                              {
-                                "tag": "span"
-                              }
-                            ]
-                          },
-                          {
-                            "tag": "span"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><template><div><span></span></div><span></span></template></body></html>",
-        "noQuirksBodyHtml": "<template><div><span></span></div><span></span></template>"
-      }
-    },
-    {
-      "data": "<body><template><script>var i = 1;</script><td></td></template>",
-      "errors": [
-        "no doctype"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "template": true,
-            "script": true,
-            "td": true
-          },
-          "template": true,
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "template",
-                    "children": [
-                      {
-                        "content": true,
-                        "children": [
-                          {
-                            "tag": "script",
-                            "children": [
-                              {
-                                "text": "var i = 1;",
-                                "no_escape": true
-                              }
-                            ]
-                          },
-                          {
-                            "tag": "td"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><template><script>var i = 1;</script><td></td></template></body></html>",
-        "noQuirksBodyHtml": "<template><script>var i = 1;</script><td></td></template>"
-      }
-    },
-    {
-      "data": "<body><template><tr><div></div></tr></template>",
-      "errors": [
-        "no doctype",
-        "foster-parented div",
-        "foster-parented /div"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "template": true,
-            "tr": true,
-            "div": true
-          },
-          "template": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "template",
-                    "children": [
-                      {
-                        "content": true,
-                        "children": [
-                          {
-                            "tag": "tr"
-                          },
-                          {
-                            "tag": "div"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><template><tr></tr><div></div></template></body></html>",
-        "noQuirksBodyHtml": "<template><tr></tr><div></div></template>"
-      }
-    },
-    {
-      "data": "<body><template><tr></tr><td></td></template>",
-      "errors": [
-        "no doctype",
-        "unexpected <td>"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "template": true,
-            "tr": true,
-            "td": true
-          },
-          "template": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "template",
-                    "children": [
-                      {
-                        "content": true,
-                        "children": [
-                          {
-                            "tag": "tr"
-                          },
-                          {
-                            "tag": "tr",
-                            "children": [
-                              {
-                                "tag": "td"
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><template><tr></tr><tr><td></td></tr></template></body></html>",
-        "noQuirksBodyHtml": "<template><tr></tr><tr><td></td></tr></template>"
-      }
-    },
-    {
-      "data": "<body><template><td></td></tr><td></td></template>",
-      "errors": [
-        "no doctype",
-        "bad </tr>"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "template": true,
-            "td": true
-          },
-          "template": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "template",
-                    "children": [
-                      {
-                        "content": true,
-                        "children": [
-                          {
-                            "tag": "td"
-                          },
-                          {
-                            "tag": "td"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><template><td></td><td></td></template></body></html>",
-        "noQuirksBodyHtml": "<template><td></td><td></td></template>"
-      }
-    },
-    {
-      "data": "<body><template><td></td><tbody><td></td></template>",
-      "errors": [
-        "no doctype",
-        "bad <tbody>"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "template": true,
-            "td": true
-          },
-          "template": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "template",
-                    "children": [
-                      {
-                        "content": true,
-                        "children": [
-                          {
-                            "tag": "td"
-                          },
-                          {
-                            "tag": "td"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><template><td></td><td></td></template></body></html>",
-        "noQuirksBodyHtml": "<template><td></td><td></td></template>"
-      }
-    },
-    {
-      "data": "<body><template><td></td><caption></caption><td></td></template>",
-      "errors": [
-        " * (1,7) missing DOCTYPE",
-        " * (1,35) unexpected start tag in table row",
-        " * (1,45) unexpected end tag in table row"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "template": true,
-            "td": true
-          },
-          "template": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "template",
-                    "children": [
-                      {
-                        "content": true,
-                        "children": [
-                          {
-                            "tag": "td"
-                          },
-                          {
-                            "tag": "td"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><template><td></td><td></td></template></body></html>",
-        "noQuirksBodyHtml": "<template><td></td><td></td></template>"
-      }
-    },
-    {
-      "data": "<body><template><td></td><colgroup></caption><td></td></template>",
-      "errors": [
-        " * (1,7) missing DOCTYPE",
-        " * (1,36) unexpected start tag in table row",
-        " * (1,46) unexpected end tag in table row"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "template": true,
-            "td": true
-          },
-          "template": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "template",
-                    "children": [
-                      {
-                        "content": true,
-                        "children": [
-                          {
-                            "tag": "td"
-                          },
-                          {
-                            "tag": "td"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><template><td></td><td></td></template></body></html>",
-        "noQuirksBodyHtml": "<template><td></td><td></td></template>"
-      }
-    },
-    {
-      "data": "<body><template><td></td></table><td></td></template>",
-      "errors": [
-        "no doctype",
-        "bad </table>"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "template": true,
-            "td": true
-          },
-          "template": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "template",
-                    "children": [
-                      {
-                        "content": true,
-                        "children": [
-                          {
-                            "tag": "td"
-                          },
-                          {
-                            "tag": "td"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><template><td></td><td></td></template></body></html>",
-        "noQuirksBodyHtml": "<template><td></td><td></td></template>"
-      }
-    },
-    {
-      "data": "<body><template><tr></tr><tbody><tr></tr></template>",
-      "errors": [
-        "no doctype",
-        "bad <tbody>"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "template": true,
-            "tr": true
-          },
-          "template": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "template",
-                    "children": [
-                      {
-                        "content": true,
-                        "children": [
-                          {
-                            "tag": "tr"
-                          },
-                          {
-                            "tag": "tr"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><template><tr></tr><tr></tr></template></body></html>",
-        "noQuirksBodyHtml": "<template><tr></tr><tr></tr></template>"
-      }
-    },
-    {
-      "data": "<body><template><tr></tr><caption><tr></tr></template>",
-      "errors": [
-        "no doctype",
-        "bad <caption>"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "template": true,
-            "tr": true
-          },
-          "template": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "template",
-                    "children": [
-                      {
-                        "content": true,
-                        "children": [
-                          {
-                            "tag": "tr"
-                          },
-                          {
-                            "tag": "tr"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><template><tr></tr><tr></tr></template></body></html>",
-        "noQuirksBodyHtml": "<template><tr></tr><tr></tr></template>"
-      }
-    },
-    {
-      "data": "<body><template><tr></tr></table><tr></tr></template>",
-      "errors": [
-        "no doctype",
-        "bad </table>"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "template": true,
-            "tr": true
-          },
-          "template": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "template",
-                    "children": [
-                      {
-                        "content": true,
-                        "children": [
-                          {
-                            "tag": "tr"
-                          },
-                          {
-                            "tag": "tr"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><template><tr></tr><tr></tr></template></body></html>",
-        "noQuirksBodyHtml": "<template><tr></tr><tr></tr></template>"
-      }
-    },
-    {
-      "data": "<body><template><thead></thead><caption></caption><tbody></tbody></template>",
-      "errors": [
-        "no doctype"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "template": true,
-            "thead": true,
-            "caption": true,
-            "tbody": true
-          },
-          "template": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "template",
-                    "children": [
-                      {
-                        "content": true,
-                        "children": [
-                          {
-                            "tag": "thead"
-                          },
-                          {
-                            "tag": "caption"
-                          },
-                          {
-                            "tag": "tbody"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><template><thead></thead><caption></caption><tbody></tbody></template></body></html>",
-        "noQuirksBodyHtml": "<template><thead></thead><caption></caption><tbody></tbody></template>"
-      }
-    },
-    {
-      "data": "<body><template><thead></thead></table><tbody></tbody></template></body>",
-      "errors": [
-        "no doctype",
-        "bad </table>"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "template": true,
-            "thead": true,
-            "tbody": true
-          },
-          "template": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "template",
-                    "children": [
-                      {
-                        "content": true,
-                        "children": [
-                          {
-                            "tag": "thead"
-                          },
-                          {
-                            "tag": "tbody"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><template><thead></thead><tbody></tbody></template></body></html>",
-        "noQuirksBodyHtml": "<template><thead></thead><tbody></tbody></template>"
-      }
-    },
-    {
-      "data": "<body><template><div><tr></tr></div></template>",
-      "errors": [
-        "no doctype",
-        "bad tr",
-        "bad /tr"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "template": true,
-            "div": true
-          },
-          "template": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "template",
-                    "children": [
-                      {
-                        "content": true,
-                        "children": [
-                          {
-                            "tag": "div"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><template><div></div></template></body></html>",
-        "noQuirksBodyHtml": "<template><div></div></template>"
-      }
-    },
-    {
-      "data": "<body><template><em>Hello</em></template>",
-      "errors": [
-        "no doctype"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "template": true,
-            "em": true
-          },
-          "template": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "template",
-                    "children": [
-                      {
-                        "content": true,
-                        "children": [
-                          {
-                            "tag": "em",
-                            "children": [
-                              {
-                                "text": "Hello"
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><template><em>Hello</em></template></body></html>",
-        "noQuirksBodyHtml": "<template><em>Hello</em></template>"
-      }
-    },
-    {
-      "data": "<body><template><!--comment--></template>",
-      "errors": [
-        "no doctype"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "template": true
-          },
-          "template": true,
-          "comment": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "template",
-                    "children": [
-                      {
-                        "content": true,
-                        "children": [
-                          {
-                            "comment": "comment"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><template><!--comment--></template></body></html>",
-        "noQuirksBodyHtml": "<template><!--comment--></template>"
-      }
-    },
-    {
-      "data": "<body><template><style></style><td></td></template>",
-      "errors": [
-        "no doctype"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "template": true,
-            "style": true,
-            "td": true
-          },
-          "template": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "template",
-                    "children": [
-                      {
-                        "content": true,
-                        "children": [
-                          {
-                            "tag": "style"
-                          },
-                          {
-                            "tag": "td"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><template><style></style><td></td></template></body></html>",
-        "noQuirksBodyHtml": "<template><style></style><td></td></template>"
-      }
-    },
-    {
-      "data": "<body><template><meta><td></td></template>",
-      "errors": [
-        "no doctype"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "template": true,
-            "meta": true,
-            "td": true
-          },
-          "template": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "template",
-                    "children": [
-                      {
-                        "content": true,
-                        "children": [
-                          {
-                            "tag": "meta"
-                          },
-                          {
-                            "tag": "td"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><template><meta><td></td></template></body></html>",
-        "noQuirksBodyHtml": "<template><meta><td></td></template>"
-      }
-    },
-    {
-      "data": "<body><template><link><td></td></template>",
-      "errors": [
-        "no doctype"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "template": true,
-            "link": true,
-            "td": true
-          },
-          "template": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "template",
-                    "children": [
-                      {
-                        "content": true,
-                        "children": [
-                          {
-                            "tag": "link"
-                          },
-                          {
-                            "tag": "td"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><template><link><td></td></template></body></html>",
-        "noQuirksBodyHtml": "<template><link><td></td></template>"
-      }
-    },
-    {
-      "data": "<body><template><template><tr></tr></template><td></td></template>",
-      "errors": [
-        "no doctype"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "template": true,
-            "tr": true,
-            "td": true
-          },
-          "template": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "template",
-                    "children": [
-                      {
-                        "content": true,
-                        "children": [
-                          {
-                            "tag": "template",
-                            "children": [
-                              {
-                                "content": true,
-                                "children": [
-                                  {
-                                    "tag": "tr"
-                                  }
-                                ]
-                              }
-                            ]
-                          },
-                          {
-                            "tag": "td"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><template><template><tr></tr></template><td></td></template></body></html>",
-        "noQuirksBodyHtml": "<template><template><tr></tr></template><td></td></template>"
-      }
-    },
-    {
-      "data": "<body><table><colgroup><template><col></col></template></colgroup></table></body>",
-      "errors": [
-        "no doctype",
-        "bad /col"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true,
-            "colgroup": true,
-            "template": true,
-            "col": true
-          },
-          "template": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "colgroup",
-                        "children": [
-                          {
-                            "tag": "template",
-                            "children": [
-                              {
-                                "content": true,
-                                "children": [
-                                  {
-                                    "tag": "col"
-                                  }
-                                ]
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><table><colgroup><template><col></template></colgroup></table></body></html>",
-        "noQuirksBodyHtml": "<table><colgroup><template><col></template></colgroup></table>"
-      }
-    },
-    {
-      "data": "<body a=b><template><div></div><body c=d><div></div></body></template></body>",
-      "errors": [
-        "no doctype",
-        "bad <body>",
-        "bad </body>"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "template": true,
-            "div": true
-          },
-          "template": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "attrs": [
-                  {
-                    "name": "a",
-                    "value": "b"
-                  }
-                ],
-                "children": [
-                  {
-                    "tag": "template",
-                    "children": [
-                      {
-                        "content": true,
-                        "children": [
-                          {
-                            "tag": "div"
-                          },
-                          {
-                            "tag": "div"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body a=\"b\"><template><div></div><div></div></template></body></html>",
-        "noQuirksBodyHtml": "<template><div></div><div></div></template>"
-      }
-    },
-    {
-      "data": "<html a=b><template><div><html b=c><span></template>",
-      "errors": [
-        "no doctype",
-        "bad <html>",
-        "missing end tags in template"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "template": true,
-            "div": true,
-            "span": true,
-            "body": true
-          },
-          "template": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "attrs": [
-              {
-                "name": "a",
-                "value": "b"
-              }
-            ],
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "template",
-                    "children": [
-                      {
-                        "content": true,
-                        "children": [
-                          {
-                            "tag": "div",
-                            "children": [
-                              {
-                                "tag": "span"
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html a=\"b\"><head><template><div><span></span></div></template></head><body></body></html>",
-        "noQuirksBodyHtml": "<template><div><span></span></div></template>"
-      }
-    },
-    {
-      "data": "<html a=b><template><col></col><html b=c><col></col></template>",
-      "errors": [
-        "no doctype",
-        "bad /col",
-        "bad html",
-        "bad /col"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "template": true,
-            "col": true,
-            "body": true
-          },
-          "template": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "attrs": [
-              {
-                "name": "a",
-                "value": "b"
-              }
-            ],
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "template",
-                    "children": [
-                      {
-                        "content": true,
-                        "children": [
-                          {
-                            "tag": "col"
-                          },
-                          {
-                            "tag": "col"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html a=\"b\"><head><template><col><col></template></head><body></body></html>",
-        "noQuirksBodyHtml": "<template><col><col></template>"
-      }
-    },
-    {
-      "data": "<html a=b><template><frame></frame><html b=c><frame></frame></template>",
-      "errors": [
-        "no doctype",
-        "bad frame",
-        "bad /frame",
-        "bad html",
-        "bad frame",
-        "bad /frame"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "template": true,
-            "body": true
-          },
-          "template": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "attrs": [
-              {
-                "name": "a",
-                "value": "b"
-              }
-            ],
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "template",
-                    "children": [
-                      {
-                        "content": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html a=\"b\"><head><template></template></head><body></body></html>",
-        "noQuirksBodyHtml": "<template></template>"
-      }
-    },
-    {
-      "data": "<body><template><tr></tr><template></template><td></td></template>",
-      "errors": [
-        "no doctype",
-        "unexpected <td>"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "template": true,
-            "tr": true,
-            "td": true
-          },
-          "template": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "template",
-                    "children": [
-                      {
-                        "content": true,
-                        "children": [
-                          {
-                            "tag": "tr"
-                          },
-                          {
-                            "tag": "template",
-                            "children": [
-                              {
-                                "content": true
-                              }
-                            ]
-                          },
-                          {
-                            "tag": "tr",
-                            "children": [
-                              {
-                                "tag": "td"
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><template><tr></tr><template></template><tr><td></td></tr></template></body></html>",
-        "noQuirksBodyHtml": "<template><tr></tr><template></template><tr><td></td></tr></template>"
-      }
-    },
-    {
-      "data": "<body><template><thead></thead><template><tr></tr></template><tr></tr><tfoot></tfoot></template>",
-      "errors": [
-        "no doctype"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "template": true,
-            "thead": true,
-            "tr": true,
-            "tbody": true,
-            "tfoot": true
-          },
-          "template": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "template",
-                    "children": [
-                      {
-                        "content": true,
-                        "children": [
-                          {
-                            "tag": "thead"
-                          },
-                          {
-                            "tag": "template",
-                            "children": [
-                              {
-                                "content": true,
-                                "children": [
-                                  {
-                                    "tag": "tr"
-                                  }
-                                ]
-                              }
-                            ]
-                          },
-                          {
-                            "tag": "tbody",
-                            "children": [
-                              {
-                                "tag": "tr"
-                              }
-                            ]
-                          },
-                          {
-                            "tag": "tfoot"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><template><thead></thead><template><tr></tr></template><tbody><tr></tr></tbody><tfoot></tfoot></template></body></html>",
-        "noQuirksBodyHtml": "<template><thead></thead><template><tr></tr></template><tbody><tr></tr></tbody><tfoot></tfoot></template>"
-      }
-    },
-    {
-      "data": "<body><template><template><b><template></template></template>text</template>",
-      "errors": [
-        "no doctype",
-        "missing </b>"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "template": true,
-            "b": true
-          },
-          "template": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "template",
-                    "children": [
-                      {
-                        "content": true,
-                        "children": [
-                          {
-                            "tag": "template",
-                            "children": [
-                              {
-                                "content": true,
-                                "children": [
-                                  {
-                                    "tag": "b",
-                                    "children": [
-                                      {
-                                        "tag": "template",
-                                        "children": [
-                                          {
-                                            "content": true
-                                          }
-                                        ]
-                                      }
-                                    ]
-                                  }
-                                ]
-                              }
-                            ]
-                          },
-                          {
-                            "text": "text"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><template><template><b><template></template></b></template>text</template></body></html>",
-        "noQuirksBodyHtml": "<template><template><b><template></template></b></template>text</template>"
-      }
-    },
-    {
-      "data": "<body><template><col><colgroup>",
-      "errors": [
-        "no doctype",
-        "bad colgroup",
-        "eof in template"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "template": true,
-            "col": true
-          },
-          "template": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "template",
-                    "children": [
-                      {
-                        "content": true,
-                        "children": [
-                          {
-                            "tag": "col"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><template><col></template></body></html>",
-        "noQuirksBodyHtml": "<template><col></template>"
-      }
-    },
-    {
-      "data": "<body><template><col></colgroup>",
-      "errors": [
-        "no doctype",
-        "bogus /colgroup",
-        "eof in template"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "template": true,
-            "col": true
-          },
-          "template": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "template",
-                    "children": [
-                      {
-                        "content": true,
-                        "children": [
-                          {
-                            "tag": "col"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><template><col></template></body></html>",
-        "noQuirksBodyHtml": "<template><col></template>"
-      }
-    },
-    {
-      "data": "<body><template><col><colgroup></template></body>",
-      "errors": [
-        "no doctype",
-        "bad colgroup"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "template": true,
-            "col": true
-          },
-          "template": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "template",
-                    "children": [
-                      {
-                        "content": true,
-                        "children": [
-                          {
-                            "tag": "col"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><template><col></template></body></html>",
-        "noQuirksBodyHtml": "<template><col></template>"
-      }
-    },
-    {
-      "data": "<body><template><col><div>",
-      "errors": [
-        " * (1,7) missing DOCTYPE",
-        " * (1,27) unexpected token",
-        " * (1,27) unexpected end of file in template"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "template": true,
-            "col": true
-          },
-          "template": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "template",
-                    "children": [
-                      {
-                        "content": true,
-                        "children": [
-                          {
-                            "tag": "col"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><template><col></template></body></html>",
-        "noQuirksBodyHtml": "<template><col></template>"
-      }
-    },
-    {
-      "data": "<body><template><col></div>",
-      "errors": [
-        "no doctype",
-        "bad /div",
-        "eof in template"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "template": true,
-            "col": true
-          },
-          "template": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "template",
-                    "children": [
-                      {
-                        "content": true,
-                        "children": [
-                          {
-                            "tag": "col"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><template><col></template></body></html>",
-        "noQuirksBodyHtml": "<template><col></template>"
-      }
-    },
-    {
-      "data": "<body><template><col>Hello",
-      "errors": [
-        "no doctype",
-        "unexpected text",
-        "eof in template"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "template": true,
-            "col": true
-          },
-          "template": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "template",
-                    "children": [
-                      {
-                        "content": true,
-                        "children": [
-                          {
-                            "tag": "col"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><template><col></template></body></html>",
-        "noQuirksBodyHtml": "<template><col></template>"
-      }
-    },
-    {
-      "data": "<body><template><i><menu>Foo</i>",
-      "errors": [
-        "no doctype",
-        "mising /menu",
-        "eof in template"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "template": true,
-            "i": true,
-            "menu": true
-          },
-          "template": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "template",
-                    "children": [
-                      {
-                        "content": true,
-                        "children": [
-                          {
-                            "tag": "i"
-                          },
-                          {
-                            "tag": "menu",
-                            "children": [
-                              {
-                                "tag": "i",
-                                "children": [
-                                  {
-                                    "text": "Foo"
-                                  }
-                                ]
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><template><i></i><menu><i>Foo</i></menu></template></body></html>",
-        "noQuirksBodyHtml": "<template><i></i><menu><i>Foo</i></menu></template>"
-      }
-    },
-    {
-      "data": "<body><template></div><div>Foo</div><template></template><tr></tr>",
-      "errors": [
-        "no doctype",
-        "bogus /div",
-        "bogus tr",
-        "bogus /tr",
-        "eof in template"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "template": true,
-            "div": true
-          },
-          "template": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "template",
-                    "children": [
-                      {
-                        "content": true,
-                        "children": [
-                          {
-                            "tag": "div",
-                            "children": [
-                              {
-                                "text": "Foo"
-                              }
-                            ]
-                          },
-                          {
-                            "tag": "template",
-                            "children": [
-                              {
-                                "content": true
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><template><div>Foo</div><template></template></template></body></html>",
-        "noQuirksBodyHtml": "<template><div>Foo</div><template></template></template>"
-      }
-    },
-    {
-      "data": "<body><div><template></div><tr><td>Foo</td></tr></template>",
-      "errors": [
-        " * (1,7) missing DOCTYPE",
-        " * (1,28) unexpected token in template",
-        " * (1,60) unexpected end of file"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "div": true,
-            "template": true,
-            "tr": true,
-            "td": true
-          },
-          "template": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "div",
-                    "children": [
-                      {
-                        "tag": "template",
-                        "children": [
-                          {
-                            "content": true,
-                            "children": [
-                              {
-                                "tag": "tr",
-                                "children": [
-                                  {
-                                    "tag": "td",
-                                    "children": [
-                                      {
-                                        "text": "Foo"
-                                      }
-                                    ]
-                                  }
-                                ]
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><div><template><tr><td>Foo</td></tr></template></div></body></html>",
-        "noQuirksBodyHtml": "<div><template><tr><td>Foo</td></tr></template></div>"
-      }
-    },
-    {
-      "data": "<template></figcaption><sub><table></table>",
-      "errors": [
-        "no doctype",
-        "bad /figcaption",
-        "eof in template"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "template": true,
-            "sub": true,
-            "table": true,
-            "body": true
-          },
-          "template": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "template",
-                    "children": [
-                      {
-                        "content": true,
-                        "children": [
-                          {
-                            "tag": "sub",
-                            "children": [
-                              {
-                                "tag": "table"
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><template><sub><table></table></sub></template></head><body></body></html>",
-        "noQuirksBodyHtml": "<template><sub><table></table></sub></template>"
-      }
-    },
-    {
-      "data": "<template><template>",
-      "errors": [
-        "no doctype",
-        "eof in template",
-        "eof in template"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "template": true,
-            "body": true
-          },
-          "template": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "template",
-                    "children": [
-                      {
-                        "content": true,
-                        "children": [
-                          {
-                            "tag": "template",
-                            "children": [
-                              {
-                                "content": true
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><template><template></template></template></head><body></body></html>",
-        "noQuirksBodyHtml": "<template><template></template></template>"
-      }
-    },
-    {
-      "data": "<template><div>",
-      "errors": [
-        "no doctype",
-        "eof in template"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "template": true,
-            "div": true,
-            "body": true
-          },
-          "template": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "template",
-                    "children": [
-                      {
-                        "content": true,
-                        "children": [
-                          {
-                            "tag": "div"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><template><div></div></template></head><body></body></html>",
-        "noQuirksBodyHtml": "<template><div></div></template>"
-      }
-    },
-    {
-      "data": "<template><template><div>",
-      "errors": [
-        "no doctype",
-        "eof in template",
-        "eof in template"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "template": true,
-            "div": true,
-            "body": true
-          },
-          "template": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "template",
-                    "children": [
-                      {
-                        "content": true,
-                        "children": [
-                          {
-                            "tag": "template",
-                            "children": [
-                              {
-                                "content": true,
-                                "children": [
-                                  {
-                                    "tag": "div"
-                                  }
-                                ]
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><template><template><div></div></template></template></head><body></body></html>",
-        "noQuirksBodyHtml": "<template><template><div></div></template></template>"
-      }
-    },
-    {
-      "data": "<template><template><table>",
-      "errors": [
-        "no doctype",
-        "eof in template",
-        "eof in template"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "template": true,
-            "table": true,
-            "body": true
-          },
-          "template": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "template",
-                    "children": [
-                      {
-                        "content": true,
-                        "children": [
-                          {
-                            "tag": "template",
-                            "children": [
-                              {
-                                "content": true,
-                                "children": [
-                                  {
-                                    "tag": "table"
-                                  }
-                                ]
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><template><template><table></table></template></template></head><body></body></html>",
-        "noQuirksBodyHtml": "<template><template><table></table></template></template>"
-      }
-    },
-    {
-      "data": "<template><template><tbody>",
-      "errors": [
-        "no doctype",
-        "eof in template",
-        "eof in template"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "template": true,
-            "tbody": true,
-            "body": true
-          },
-          "template": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "template",
-                    "children": [
-                      {
-                        "content": true,
-                        "children": [
-                          {
-                            "tag": "template",
-                            "children": [
-                              {
-                                "content": true,
-                                "children": [
-                                  {
-                                    "tag": "tbody"
-                                  }
-                                ]
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><template><template><tbody></tbody></template></template></head><body></body></html>",
-        "noQuirksBodyHtml": "<template><template><tbody></tbody></template></template>"
-      }
-    },
-    {
-      "data": "<template><template><tr>",
-      "errors": [
-        "no doctype",
-        "eof in template",
-        "eof in template"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "template": true,
-            "tr": true,
-            "body": true
-          },
-          "template": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "template",
-                    "children": [
-                      {
-                        "content": true,
-                        "children": [
-                          {
-                            "tag": "template",
-                            "children": [
-                              {
-                                "content": true,
-                                "children": [
-                                  {
-                                    "tag": "tr"
-                                  }
-                                ]
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><template><template><tr></tr></template></template></head><body></body></html>",
-        "noQuirksBodyHtml": "<template><template><tr></tr></template></template>"
-      }
-    },
-    {
-      "data": "<template><template><td>",
-      "errors": [
-        "no doctype",
-        "eof in template",
-        "eof in template"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "template": true,
-            "td": true,
-            "body": true
-          },
-          "template": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "template",
-                    "children": [
-                      {
-                        "content": true,
-                        "children": [
-                          {
-                            "tag": "template",
-                            "children": [
-                              {
-                                "content": true,
-                                "children": [
-                                  {
-                                    "tag": "td"
-                                  }
-                                ]
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><template><template><td></td></template></template></head><body></body></html>",
-        "noQuirksBodyHtml": "<template><template><td></td></template></template>"
-      }
-    },
-    {
-      "data": "<template><template><caption>",
-      "errors": [
-        "no doctype",
-        "eof in template",
-        "eof in template"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "template": true,
-            "caption": true,
-            "body": true
-          },
-          "template": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "template",
-                    "children": [
-                      {
-                        "content": true,
-                        "children": [
-                          {
-                            "tag": "template",
-                            "children": [
-                              {
-                                "content": true,
-                                "children": [
-                                  {
-                                    "tag": "caption"
-                                  }
-                                ]
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><template><template><caption></caption></template></template></head><body></body></html>",
-        "noQuirksBodyHtml": "<template><template><caption></caption></template></template>"
-      }
-    },
-    {
-      "data": "<template><template><colgroup>",
-      "errors": [
-        "no doctype",
-        "eof in template",
-        "eof in template"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "template": true,
-            "colgroup": true,
-            "body": true
-          },
-          "template": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "template",
-                    "children": [
-                      {
-                        "content": true,
-                        "children": [
-                          {
-                            "tag": "template",
-                            "children": [
-                              {
-                                "content": true,
-                                "children": [
-                                  {
-                                    "tag": "colgroup"
-                                  }
-                                ]
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><template><template><colgroup></colgroup></template></template></head><body></body></html>",
-        "noQuirksBodyHtml": "<template><template><colgroup></colgroup></template></template>"
-      }
-    },
-    {
-      "data": "<template><template><col>",
-      "errors": [
-        "no doctype",
-        "eof in template",
-        "eof in template"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "template": true,
-            "col": true,
-            "body": true
-          },
-          "template": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "template",
-                    "children": [
-                      {
-                        "content": true,
-                        "children": [
-                          {
-                            "tag": "template",
-                            "children": [
-                              {
-                                "content": true,
-                                "children": [
-                                  {
-                                    "tag": "col"
-                                  }
-                                ]
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><template><template><col></template></template></head><body></body></html>",
-        "noQuirksBodyHtml": "<template><template><col></template></template>"
-      }
-    },
-    {
-      "data": "<template><template><tbody><select>",
-      "errors": [
-        " * (1,11) missing DOCTYPE",
-        " * (1,36) unexpected token in table - foster parenting",
-        " * (1,36) unexpected end of file in template",
-        " * (1,36) unexpected end of file in template"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "template": true,
-            "tbody": true,
-            "select": true,
-            "body": true
-          },
-          "template": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "template",
-                    "children": [
-                      {
-                        "content": true,
-                        "children": [
-                          {
-                            "tag": "template",
-                            "children": [
-                              {
-                                "content": true,
-                                "children": [
-                                  {
-                                    "tag": "tbody"
-                                  },
-                                  {
-                                    "tag": "select"
-                                  }
-                                ]
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><template><template><tbody></tbody><select></select></template></template></head><body></body></html>",
-        "noQuirksBodyHtml": "<template><template><tbody></tbody><select></select></template></template>"
-      }
-    },
-    {
-      "data": "<template><template><table>Foo",
-      "errors": [
-        "no doctype",
-        "foster-parenting text F",
-        "foster-parenting text o",
-        "foster-parenting text o",
-        "eof",
-        "eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "template": true,
-            "table": true,
-            "body": true
-          },
-          "template": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "template",
-                    "children": [
-                      {
-                        "content": true,
-                        "children": [
-                          {
-                            "tag": "template",
-                            "children": [
-                              {
-                                "content": true,
-                                "children": [
-                                  {
-                                    "text": "Foo"
-                                  },
-                                  {
-                                    "tag": "table"
-                                  }
-                                ]
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><template><template>Foo<table></table></template></template></head><body></body></html>",
-        "noQuirksBodyHtml": "<template><template>Foo<table></table></template></template>"
-      }
-    },
-    {
-      "data": "<template><template><frame>",
-      "errors": [
-        "no doctype",
-        "bad tag",
-        "eof",
-        "eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "template": true,
-            "body": true
-          },
-          "template": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "template",
-                    "children": [
-                      {
-                        "content": true,
-                        "children": [
-                          {
-                            "tag": "template",
-                            "children": [
-                              {
-                                "content": true
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><template><template></template></template></head><body></body></html>",
-        "noQuirksBodyHtml": "<template><template></template></template>"
-      }
-    },
-    {
-      "data": "<template><template><script>var i",
-      "errors": [
-        "no doctype",
-        "eof in script",
-        "eof in template",
-        "eof in template"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "template": true,
-            "script": true,
-            "body": true
-          },
-          "template": true,
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "template",
-                    "children": [
-                      {
-                        "content": true,
-                        "children": [
-                          {
-                            "tag": "template",
-                            "children": [
-                              {
-                                "content": true,
-                                "children": [
-                                  {
-                                    "tag": "script",
-                                    "children": [
-                                      {
-                                        "text": "var i",
-                                        "no_escape": true
-                                      }
-                                    ]
-                                  }
-                                ]
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><template><template><script>var i</script></template></template></head><body></body></html>",
-        "noQuirksBodyHtml": "<template><template><script>var i</script></template></template>"
-      }
-    },
-    {
-      "data": "<template><template><style>var i",
-      "errors": [
-        "no doctype",
-        "eof in style",
-        "eof in template",
-        "eof in template"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "template": true,
-            "style": true,
-            "body": true
-          },
-          "template": true,
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "template",
-                    "children": [
-                      {
-                        "content": true,
-                        "children": [
-                          {
-                            "tag": "template",
-                            "children": [
-                              {
-                                "content": true,
-                                "children": [
-                                  {
-                                    "tag": "style",
-                                    "children": [
-                                      {
-                                        "text": "var i",
-                                        "no_escape": true
-                                      }
-                                    ]
-                                  }
-                                ]
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><template><template><style>var i</style></template></template></head><body></body></html>",
-        "noQuirksBodyHtml": "<template><template><style>var i</style></template></template>"
-      }
-    },
-    {
-      "data": "<template><table></template><body><span>Foo",
-      "errors": [
-        "no doctype",
-        "missing /table",
-        "bad eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "template": true,
-            "table": true,
-            "body": true,
-            "span": true
-          },
-          "template": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "template",
-                    "children": [
-                      {
-                        "content": true,
-                        "children": [
-                          {
-                            "tag": "table"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "span",
-                    "children": [
-                      {
-                        "text": "Foo"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><template><table></table></template></head><body><span>Foo</span></body></html>",
-        "noQuirksBodyHtml": "<template><table></table></template><span>Foo</span>"
-      }
-    },
-    {
-      "data": "<template><td></template><body><span>Foo",
-      "errors": [
-        "no doctype",
-        "bad eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "template": true,
-            "td": true,
-            "body": true,
-            "span": true
-          },
-          "template": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "template",
-                    "children": [
-                      {
-                        "content": true,
-                        "children": [
-                          {
-                            "tag": "td"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "span",
-                    "children": [
-                      {
-                        "text": "Foo"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><template><td></td></template></head><body><span>Foo</span></body></html>",
-        "noQuirksBodyHtml": "<template><td></td></template><span>Foo</span>"
-      }
-    },
-    {
-      "data": "<template><object></template><body><span>Foo",
-      "errors": [
-        "no doctype",
-        "missing /object",
-        "bad eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "template": true,
-            "object": true,
-            "body": true,
-            "span": true
-          },
-          "template": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "template",
-                    "children": [
-                      {
-                        "content": true,
-                        "children": [
-                          {
-                            "tag": "object"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "span",
-                    "children": [
-                      {
-                        "text": "Foo"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><template><object></object></template></head><body><span>Foo</span></body></html>",
-        "noQuirksBodyHtml": "<template><object></object></template><span>Foo</span>"
-      }
-    },
-    {
-      "data": "<template><svg><template>",
-      "errors": [
-        "no doctype",
-        "eof in template"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "template": true,
-            "svg svg": true,
-            "svg template": true,
-            "body": true
-          },
-          "template": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "template",
-                    "children": [
-                      {
-                        "content": true,
-                        "children": [
-                          {
-                            "tag": "svg",
-                            "ns": "http://www.w3.org/2000/svg",
-                            "children": [
-                              {
-                                "tag": "template",
-                                "ns": "http://www.w3.org/2000/svg"
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><template><svg><template></template></svg></template></head><body></body></html>",
-        "noQuirksBodyHtml": "<template><svg><template></template></svg></template>"
-      }
-    },
-    {
-      "data": "<template><svg><foo><template><foreignObject><div></template><div>",
-      "errors": [
-        "no doctype",
-        "ugly template closure",
-        "bad eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "template": true,
-            "svg svg": true,
-            "svg foo": true,
-            "svg template": true,
-            "svg foreignObject": true,
-            "div": true,
-            "body": true
-          },
-          "template": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "template",
-                    "children": [
-                      {
-                        "content": true,
-                        "children": [
-                          {
-                            "tag": "svg",
-                            "ns": "http://www.w3.org/2000/svg",
-                            "children": [
-                              {
-                                "tag": "foo",
-                                "ns": "http://www.w3.org/2000/svg",
-                                "children": [
-                                  {
-                                    "tag": "template",
-                                    "ns": "http://www.w3.org/2000/svg",
-                                    "children": [
-                                      {
-                                        "tag": "foreignObject",
-                                        "ns": "http://www.w3.org/2000/svg",
-                                        "children": [
-                                          {
-                                            "tag": "div"
-                                          }
-                                        ]
-                                      }
-                                    ]
-                                  }
-                                ]
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "div"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><template><svg><foo><template><foreignObject><div></div></foreignObject></template></foo></svg></template></head><body><div></div></body></html>",
-        "noQuirksBodyHtml": "<template><svg><foo><template><foreignObject><div></div></foreignObject></template></foo></svg></template><div></div>"
-      }
-    },
-    {
-      "data": "<dummy><template><span></dummy>",
-      "errors": [
-        "no doctype",
-        "bad end tag </dummy>",
-        "eof in template",
-        "eof in dummy"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "dummy": true,
-            "template": true,
-            "span": true
-          },
-          "template": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "dummy",
-                    "children": [
-                      {
-                        "tag": "template",
-                        "children": [
-                          {
-                            "content": true,
-                            "children": [
-                              {
-                                "tag": "span"
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><dummy><template><span></span></template></dummy></body></html>",
-        "noQuirksBodyHtml": "<dummy><template><span></span></template></dummy>"
-      }
-    },
-    {
-      "data": "<body><table><tr><td><select><template>Foo</template><caption>A</table>",
-      "errors": [
-        "no doctype",
-        "(1,62): unexpected-caption-in-select-in-table"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true,
-            "tbody": true,
-            "tr": true,
-            "td": true,
-            "select": true,
-            "template": true,
-            "caption": true
-          },
-          "template": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "tbody",
-                        "children": [
-                          {
-                            "tag": "tr",
-                            "children": [
-                              {
-                                "tag": "td",
-                                "children": [
-                                  {
-                                    "tag": "select",
-                                    "children": [
-                                      {
-                                        "tag": "template",
-                                        "children": [
-                                          {
-                                            "content": true,
-                                            "children": [
-                                              {
-                                                "text": "Foo"
-                                              }
-                                            ]
-                                          }
-                                        ]
-                                      }
-                                    ]
-                                  }
-                                ]
-                              }
-                            ]
-                          }
-                        ]
-                      },
-                      {
-                        "tag": "caption",
-                        "children": [
-                          {
-                            "text": "A"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><table><tbody><tr><td><select><template>Foo</template></select></td></tr></tbody><caption>A</caption></table></body></html>",
-        "noQuirksBodyHtml": "<table><tbody><tr><td><select><template>Foo</template></select></td></tr></tbody><caption>A</caption></table>"
-      }
-    },
-    {
-      "data": "<body></body><template>",
-      "errors": [
-        "no doctype",
-        "(1,23): template-after-body",
-        "(1,24): eof-in-template"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "template": true
-          },
-          "template": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "template",
-                    "children": [
-                      {
-                        "content": true
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><template></template></body></html>",
-        "noQuirksBodyHtml": "<template></template>"
-      }
-    },
-    {
-      "data": "<head></head><template>",
-      "errors": [
-        "no doctype",
-        "(1,23): template-after-head",
-        "(1,24): eof-in-template"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "template": true,
-            "body": true
-          },
-          "template": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "template",
-                    "children": [
-                      {
-                        "content": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><template></template></head><body></body></html>",
-        "noQuirksBodyHtml": "<template></template>"
-      }
-    },
-    {
-      "data": "<head></head><template>Foo</template>",
-      "errors": [
-        "no doctype",
-        "(1,23): template-after-head"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "template": true,
-            "body": true
-          },
-          "template": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "template",
-                    "children": [
-                      {
-                        "content": true,
-                        "children": [
-                          {
-                            "text": "Foo"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><template>Foo</template></head><body></body></html>",
-        "noQuirksBodyHtml": "<template>Foo</template>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE HTML><dummy><table><template><table><template><table><script>",
-      "errors": [
-        "eof script",
-        "eof template",
-        "eof template",
-        "eof table"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "dummy": true,
-            "table": true,
-            "template": true,
-            "script": true
-          },
-          "doctype": true,
-          "template": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "dummy",
-                    "children": [
-                      {
-                        "tag": "table",
-                        "children": [
-                          {
-                            "tag": "template",
-                            "children": [
-                              {
-                                "content": true,
-                                "children": [
-                                  {
-                                    "tag": "table",
-                                    "children": [
-                                      {
-                                        "tag": "template",
-                                        "children": [
-                                          {
-                                            "content": true,
-                                            "children": [
-                                              {
-                                                "tag": "table",
-                                                "children": [
-                                                  {
-                                                    "tag": "script"
-                                                  }
-                                                ]
-                                              }
-                                            ]
-                                          }
-                                        ]
-                                      }
-                                    ]
-                                  }
-                                ]
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><dummy><table><template><table><template><table><script></script></table></template></table></template></table></dummy></body></html>",
-        "noQuirksBodyHtml": "<dummy><table><template><table><template><table><script></script></table></template></table></template></table></dummy>"
-      }
-    },
-    {
-      "data": "<template><a><table><a>",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "template": true,
-            "a": true,
-            "table": true,
-            "body": true
-          },
-          "template": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "template",
-                    "children": [
-                      {
-                        "content": true,
-                        "children": [
-                          {
-                            "tag": "a",
-                            "children": [
-                              {
-                                "tag": "a"
-                              },
-                              {
-                                "tag": "table"
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><template><a><a></a><table></table></a></template></head><body></body></html>",
-        "noQuirksBodyHtml": "<template><a><a></a><table></table></a></template>"
-      }
-    }
-  ],
-  "tests1.dat": [
-    {
-      "data": "Test",
-      "errors": [
-        "(1,0): expected-doctype-but-got-chars"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "Test"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>Test</body></html>",
-        "noQuirksBodyHtml": "Test"
-      }
-    },
-    {
-      "data": "<p>One<p>Two",
-      "errors": [
-        "(1,3): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "p": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "p",
-                    "children": [
-                      {
-                        "text": "One"
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "p",
-                    "children": [
-                      {
-                        "text": "Two"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><p>One</p><p>Two</p></body></html>",
-        "noQuirksBodyHtml": "<p>One</p><p>Two</p>"
-      }
-    },
-    {
-      "data": "Line1<br>Line2<br>Line3<br>Line4",
-      "errors": [
-        "(1,0): expected-doctype-but-got-chars"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "br": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "Line1"
-                  },
-                  {
-                    "tag": "br"
-                  },
-                  {
-                    "text": "Line2"
-                  },
-                  {
-                    "tag": "br"
-                  },
-                  {
-                    "text": "Line3"
-                  },
-                  {
-                    "tag": "br"
-                  },
-                  {
-                    "text": "Line4"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>Line1<br>Line2<br>Line3<br>Line4</body></html>",
-        "noQuirksBodyHtml": "Line1<br>Line2<br>Line3<br>Line4"
-      }
-    },
-    {
-      "data": "<html>",
-      "errors": [
-        "(1,6): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body></body></html>",
-        "noQuirksBodyHtml": ""
-      }
-    },
-    {
-      "data": "<head>",
-      "errors": [
-        "(1,6): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body></body></html>",
-        "noQuirksBodyHtml": ""
-      }
-    },
-    {
-      "data": "<body>",
-      "errors": [
-        "(1,6): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body></body></html>",
-        "noQuirksBodyHtml": ""
-      }
-    },
-    {
-      "data": "<html><head>",
-      "errors": [
-        "(1,6): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body></body></html>",
-        "noQuirksBodyHtml": ""
-      }
-    },
-    {
-      "data": "<html><head></head>",
-      "errors": [
-        "(1,6): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body></body></html>",
-        "noQuirksBodyHtml": ""
-      }
-    },
-    {
-      "data": "<html><head></head><body>",
-      "errors": [
-        "(1,6): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body></body></html>",
-        "noQuirksBodyHtml": ""
-      }
-    },
-    {
-      "data": "<html><head></head><body></body>",
-      "errors": [
-        "(1,6): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body></body></html>",
-        "noQuirksBodyHtml": ""
-      }
-    },
-    {
-      "data": "<html><head><body></body></html>",
-      "errors": [
-        "(1,6): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body></body></html>",
-        "noQuirksBodyHtml": ""
-      }
-    },
-    {
-      "data": "<html><head></body></html>",
-      "errors": [
-        "(1,6): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body></body></html>",
-        "noQuirksBodyHtml": ""
-      }
-    },
-    {
-      "data": "<html><head><body></html>",
-      "errors": [
-        "(1,6): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body></body></html>",
-        "noQuirksBodyHtml": ""
-      }
-    },
-    {
-      "data": "<html><body></html>",
-      "errors": [
-        "(1,6): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body></body></html>",
-        "noQuirksBodyHtml": ""
-      }
-    },
-    {
-      "data": "<body></html>",
-      "errors": [
-        "(1,6): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body></body></html>",
-        "noQuirksBodyHtml": ""
-      }
-    },
-    {
-      "data": "<head></html>",
-      "errors": [
-        "(1,6): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body></body></html>",
-        "noQuirksBodyHtml": ""
-      }
-    },
-    {
-      "data": "</head>",
-      "errors": [
-        "(1,7): expected-doctype-but-got-end-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body></body></html>",
-        "noQuirksBodyHtml": ""
-      }
-    },
-    {
-      "data": "</body>",
-      "errors": [
-        "(1,7): expected-doctype-but-got-end-tag element."
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body></body></html>",
-        "noQuirksBodyHtml": ""
-      }
-    },
-    {
-      "data": "</html>",
-      "errors": [
-        "(1,7): expected-doctype-but-got-end-tag element."
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body></body></html>",
-        "noQuirksBodyHtml": ""
-      }
-    },
-    {
-      "data": "<b><table><td><i></table>",
-      "errors": [
-        "(1,3): expected-doctype-but-got-start-tag",
-        "(1,14): unexpected-cell-in-table-body",
-        "(1,25): unexpected-cell-end-tag",
-        "(1,25): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "b": true,
-            "table": true,
-            "tbody": true,
-            "tr": true,
-            "td": true,
-            "i": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "b",
-                    "children": [
-                      {
-                        "tag": "table",
-                        "children": [
-                          {
-                            "tag": "tbody",
-                            "children": [
-                              {
-                                "tag": "tr",
-                                "children": [
-                                  {
-                                    "tag": "td",
-                                    "children": [
-                                      {
-                                        "tag": "i"
-                                      }
-                                    ]
-                                  }
-                                ]
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><b><table><tbody><tr><td><i></i></td></tr></tbody></table></b></body></html>",
-        "noQuirksBodyHtml": "<b><table><tbody><tr><td><i></i></td></tr></tbody></table></b>"
-      }
-    },
-    {
-      "data": "<b><table><td></b><i></table>X",
-      "errors": [
-        "(1,3): expected-doctype-but-got-start-tag",
-        "(1,14): unexpected-cell-in-table-body",
-        "(1,18): unexpected-end-tag",
-        "(1,29): unexpected-cell-end-tag",
-        "(1,30): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "b": true,
-            "table": true,
-            "tbody": true,
-            "tr": true,
-            "td": true,
-            "i": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "b",
-                    "children": [
-                      {
-                        "tag": "table",
-                        "children": [
-                          {
-                            "tag": "tbody",
-                            "children": [
-                              {
-                                "tag": "tr",
-                                "children": [
-                                  {
-                                    "tag": "td",
-                                    "children": [
-                                      {
-                                        "tag": "i"
-                                      }
-                                    ]
-                                  }
-                                ]
-                              }
-                            ]
-                          }
-                        ]
-                      },
-                      {
-                        "text": "X"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><b><table><tbody><tr><td><i></i></td></tr></tbody></table>X</b></body></html>",
-        "noQuirksBodyHtml": "<b><table><tbody><tr><td><i></i></td></tr></tbody></table>X</b>"
-      }
-    },
-    {
-      "data": "<h1>Hello<h2>World",
-      "errors": [
-        "(1,4): expected-doctype-but-got-start-tag",
-        "(1,13): unexpected-start-tag",
-        "(1,18): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "h1": true,
-            "h2": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "h1",
-                    "children": [
-                      {
-                        "text": "Hello"
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "h2",
-                    "children": [
-                      {
-                        "text": "World"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><h1>Hello</h1><h2>World</h2></body></html>",
-        "noQuirksBodyHtml": "<h1>Hello</h1><h2>World</h2>"
-      }
-    },
-    {
-      "data": "<a><p>X<a>Y</a>Z</p></a>",
-      "errors": [
-        "(1,3): expected-doctype-but-got-start-tag",
-        "(1,10): unexpected-start-tag-implies-end-tag",
-        "(1,10): adoption-agency-1.3",
-        "(1,24): unexpected-end-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "a": true,
-            "p": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "a"
-                  },
-                  {
-                    "tag": "p",
-                    "children": [
-                      {
-                        "tag": "a",
-                        "children": [
-                          {
-                            "text": "X"
-                          }
-                        ]
-                      },
-                      {
-                        "tag": "a",
-                        "children": [
-                          {
-                            "text": "Y"
-                          }
-                        ]
-                      },
-                      {
-                        "text": "Z"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><a></a><p><a>X</a><a>Y</a>Z</p></body></html>",
-        "noQuirksBodyHtml": "<a></a><p><a>X</a><a>Y</a>Z</p>"
-      }
-    },
-    {
-      "data": "<b><button>foo</b>bar",
-      "errors": [
-        "(1,3): expected-doctype-but-got-start-tag",
-        "(1,18): adoption-agency-1.3",
-        "(1,21): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "b": true,
-            "button": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "b"
-                  },
-                  {
-                    "tag": "button",
-                    "children": [
-                      {
-                        "tag": "b",
-                        "children": [
-                          {
-                            "text": "foo"
-                          }
-                        ]
-                      },
-                      {
-                        "text": "bar"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><b></b><button><b>foo</b>bar</button></body></html>",
-        "noQuirksBodyHtml": "<b></b><button><b>foo</b>bar</button>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><span><button>foo</span>bar",
-      "errors": [
-        "(1,39): unexpected-end-tag",
-        "(1,42): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "span": true,
-            "button": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "span",
-                    "children": [
-                      {
-                        "tag": "button",
-                        "children": [
-                          {
-                            "text": "foobar"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><span><button>foobar</button></span></body></html>",
-        "noQuirksBodyHtml": "<span><button>foobar</button></span>"
-      }
-    },
-    {
-      "data": "<p><b><div><marquee></p></b></div>X",
-      "errors": [
-        "(1,3): expected-doctype-but-got-start-tag",
-        "(1,11): unexpected-end-tag",
-        "(1,24): unexpected-end-tag",
-        "(1,28): unexpected-end-tag",
-        "(1,34): end-tag-too-early",
-        "(1,35): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "p": true,
-            "b": true,
-            "div": true,
-            "marquee": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "p",
-                    "children": [
-                      {
-                        "tag": "b"
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "div",
-                    "children": [
-                      {
-                        "tag": "b",
-                        "children": [
-                          {
-                            "tag": "marquee",
-                            "children": [
-                              {
-                                "tag": "p"
-                              },
-                              {
-                                "text": "X"
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><p><b></b></p><div><b><marquee><p></p>X</marquee></b></div></body></html>",
-        "noQuirksBodyHtml": "<p><b></b></p><div><b><marquee><p></p>X</marquee></b></div>"
-      }
-    },
-    {
-      "data": "<script><div></script></div><title><p></title><p><p>",
-      "errors": [
-        "(1,8): expected-doctype-but-got-start-tag",
-        "(1,28): unexpected-end-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "title": true,
-            "body": true,
-            "p": true
-          },
-          "no_escape": true,
-          "escaped": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "<div>",
-                        "no_escape": true
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "title",
-                    "children": [
-                      {
-                        "text": "<p>",
-                        "escaped": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "p"
-                  },
-                  {
-                    "tag": "p"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><script><div></script><title>&lt;p&gt;</title></head><body><p></p><p></p></body></html>",
-        "noQuirksBodyHtml": "<script><div></script><title>&lt;p&gt;</title><p></p><p></p>"
-      }
-    },
-    {
-      "data": "<!--><div>--<!-->",
-      "errors": [
-        "(1,5): incorrect-comment",
-        "(1,10): expected-doctype-but-got-start-tag",
-        "(1,17): incorrect-comment",
-        "(1,17): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "div": true
-          },
-          "comment": true
-        },
-        "tree": [
-          {
-            "comment": ""
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "div",
-                    "children": [
-                      {
-                        "text": "--"
-                      },
-                      {
-                        "comment": ""
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!----><html><head></head><body><div>--<!----></div></body></html>",
-        "noQuirksBodyHtml": "<!----><div>--<!----></div>"
-      }
-    },
-    {
-      "data": "<p><hr></p>",
-      "errors": [
-        "(1,3): expected-doctype-but-got-start-tag",
-        "(1,11): unexpected-end-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "p": true,
-            "hr": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "p"
-                  },
-                  {
-                    "tag": "hr"
-                  },
-                  {
-                    "tag": "p"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><p></p><hr><p></p></body></html>",
-        "noQuirksBodyHtml": "<p></p><hr><p></p>"
-      }
-    },
-    {
-      "data": "<select><b><option><select><option></b></select>X",
-      "errors": [
-        "(1,8): expected-doctype-but-got-start-tag",
-        "(1,11): unexpected-start-tag-in-select",
-        "(1,27): unexpected-select-in-select",
-        "(1,39): unexpected-end-tag",
-        "(1,48): unexpected-end-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "select": true,
-            "option": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "select",
-                    "children": [
-                      {
-                        "tag": "option"
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "option",
-                    "children": [
-                      {
-                        "text": "X"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><select><option></option></select><option>X</option></body></html>",
-        "noQuirksBodyHtml": "<select><option></option></select><option>X</option>"
-      }
-    },
-    {
-      "data": "<a><table><td><a><table></table><a></tr><a></table><b>X</b>C<a>Y",
-      "errors": [
-        "(1,3): expected-doctype-but-got-start-tag",
-        "(1,14): unexpected-cell-in-table-body",
-        "(1,35): unexpected-start-tag-implies-end-tag",
-        "(1,40): unexpected-cell-end-tag",
-        "(1,43): unexpected-start-tag-implies-table-voodoo",
-        "(1,43): unexpected-start-tag-implies-end-tag",
-        "(1,43): unexpected-end-tag",
-        "(1,63): unexpected-start-tag-implies-end-tag",
-        "(1,64): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "a": true,
-            "table": true,
-            "tbody": true,
-            "tr": true,
-            "td": true,
-            "b": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "a",
-                    "children": [
-                      {
-                        "tag": "a"
-                      },
-                      {
-                        "tag": "table",
-                        "children": [
-                          {
-                            "tag": "tbody",
-                            "children": [
-                              {
-                                "tag": "tr",
-                                "children": [
-                                  {
-                                    "tag": "td",
-                                    "children": [
-                                      {
-                                        "tag": "a",
-                                        "children": [
-                                          {
-                                            "tag": "table"
-                                          }
-                                        ]
-                                      },
-                                      {
-                                        "tag": "a"
-                                      }
-                                    ]
-                                  }
-                                ]
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "a",
-                    "children": [
-                      {
-                        "tag": "b",
-                        "children": [
-                          {
-                            "text": "X"
-                          }
-                        ]
-                      },
-                      {
-                        "text": "C"
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "a",
-                    "children": [
-                      {
-                        "text": "Y"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><a><a></a><table><tbody><tr><td><a><table></table></a><a></a></td></tr></tbody></table></a><a><b>X</b>C</a><a>Y</a></body></html>",
-        "noQuirksBodyHtml": "<a><a></a><table><tbody><tr><td><a><table></table></a><a></a></td></tr></tbody></table></a><a><b>X</b>C</a><a>Y</a>"
-      }
-    },
-    {
-      "data": "<a X>0<b>1<a Y>2",
-      "errors": [
-        "(1,5): expected-doctype-but-got-start-tag",
-        "(1,15): unexpected-start-tag-implies-end-tag",
-        "(1,15): adoption-agency-1.3",
-        "(1,16): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "a": true,
-            "b": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "a",
-                    "attrs": [
-                      {
-                        "name": "x",
-                        "value": ""
-                      }
-                    ],
-                    "children": [
-                      {
-                        "text": "0"
-                      },
-                      {
-                        "tag": "b",
-                        "children": [
-                          {
-                            "text": "1"
-                          }
-                        ]
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "b",
-                    "children": [
-                      {
-                        "tag": "a",
-                        "attrs": [
-                          {
-                            "name": "y",
-                            "value": ""
-                          }
-                        ],
-                        "children": [
-                          {
-                            "text": "2"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><a x=\"\">0<b>1</b></a><b><a y=\"\">2</a></b></body></html>",
-        "noQuirksBodyHtml": "<a x=\"\">0<b>1</b></a><b><a y=\"\">2</a></b>"
-      }
-    },
-    {
-      "data": "<!-----><font><div>hello<table>excite!<b>me!<th><i>please!</tr><!--X-->",
-      "errors": [
-        "(1,7): unexpected-dash-after-double-dash-in-comment",
-        "(1,14): expected-doctype-but-got-start-tag",
-        "(1,41): unexpected-start-tag-implies-table-voodoo",
-        "(1,48): foster-parenting-character-in-table",
-        "(1,48): foster-parenting-character-in-table",
-        "(1,48): foster-parenting-character-in-table",
-        "(1,48): foster-parenting-character-in-table",
-        "(1,48): foster-parenting-character-in-table",
-        "(1,48): foster-parenting-character-in-table",
-        "(1,48): foster-parenting-character-in-table",
-        "(1,48): foster-parenting-character-in-table",
-        "(1,48): foster-parenting-character-in-table",
-        "(1,48): foster-parenting-character-in-table",
-        "(1,48): unexpected-cell-in-table-body",
-        "(1,63): unexpected-cell-end-tag",
-        "(1,71): eof-in-table"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "font": true,
-            "div": true,
-            "b": true,
-            "table": true,
-            "tbody": true,
-            "tr": true,
-            "th": true,
-            "i": true
-          },
-          "comment": true
-        },
-        "tree": [
-          {
-            "comment": "-"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "font",
-                    "children": [
-                      {
-                        "tag": "div",
-                        "children": [
-                          {
-                            "text": "helloexcite!"
-                          },
-                          {
-                            "tag": "b",
-                            "children": [
-                              {
-                                "text": "me!"
-                              }
-                            ]
-                          },
-                          {
-                            "tag": "table",
-                            "children": [
-                              {
-                                "tag": "tbody",
-                                "children": [
-                                  {
-                                    "tag": "tr",
-                                    "children": [
-                                      {
-                                        "tag": "th",
-                                        "children": [
-                                          {
-                                            "tag": "i",
-                                            "children": [
-                                              {
-                                                "text": "please!"
-                                              }
-                                            ]
-                                          }
-                                        ]
-                                      }
-                                    ]
-                                  },
-                                  {
-                                    "comment": "X"
-                                  }
-                                ]
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!-----><html><head></head><body><font><div>helloexcite!<b>me!</b><table><tbody><tr><th><i>please!</i></th></tr><!--X--></tbody></table></div></font></body></html>",
-        "noQuirksBodyHtml": "<!-----><font><div>helloexcite!<b>me!</b><table><tbody><tr><th><i>please!</i></th></tr><!--X--></tbody></table></div></font>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><li>hello<li>world<ul>how<li>do</ul>you</body><!--do-->",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "li": true,
-            "ul": true
-          },
-          "doctype": true,
-          "comment": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "li",
-                    "children": [
-                      {
-                        "text": "hello"
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "li",
-                    "children": [
-                      {
-                        "text": "world"
-                      },
-                      {
-                        "tag": "ul",
-                        "children": [
-                          {
-                            "text": "how"
-                          },
-                          {
-                            "tag": "li",
-                            "children": [
-                              {
-                                "text": "do"
-                              }
-                            ]
-                          }
-                        ]
-                      },
-                      {
-                        "text": "you"
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "comment": "do"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><li>hello</li><li>world<ul>how<li>do</li></ul>you</li></body><!--do--></html>",
-        "noQuirksBodyHtml": "<li>hello</li><li>world<ul>how<li>do</li></ul>you<!--do--></li>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html>A<option>B<optgroup>C<select>D</option>E",
-      "errors": [
-        "(1,54): unexpected-end-tag-in-select",
-        "(1,55): eof-in-select"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "option": true,
-            "optgroup": true,
-            "select": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "A"
-                  },
-                  {
-                    "tag": "option",
-                    "children": [
-                      {
-                        "text": "B"
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "optgroup",
-                    "children": [
-                      {
-                        "text": "C"
-                      },
-                      {
-                        "tag": "select",
-                        "children": [
-                          {
-                            "text": "DE"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body>A<option>B</option><optgroup>C<select>DE</select></optgroup></body></html>",
-        "noQuirksBodyHtml": "A<option>B</option><optgroup>C<select>DE</select></optgroup>"
-      }
-    },
-    {
-      "data": "<",
-      "errors": [
-        "(1,1): expected-tag-name",
-        "(1,1): expected-doctype-but-got-chars"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "escaped": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "<",
-                    "escaped": true
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>&lt;</body></html>",
-        "noQuirksBodyHtml": "&lt;"
-      }
-    },
-    {
-      "data": "<#",
-      "errors": [
-        "(1,1): expected-tag-name",
-        "(1,1): expected-doctype-but-got-chars"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "escaped": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "<#",
-                    "escaped": true
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>&lt;#</body></html>",
-        "noQuirksBodyHtml": "&lt;#"
-      }
-    },
-    {
-      "data": "</",
-      "errors": [
-        "(1,2): expected-closing-tag-but-got-eof",
-        "(1,2): expected-doctype-but-got-chars"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "escaped": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "</",
-                    "escaped": true
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>&lt;/</body></html>",
-        "noQuirksBodyHtml": "&lt;/"
-      }
-    },
-    {
-      "data": "</#",
-      "errors": [
-        "(1,2): expected-closing-tag-but-got-char",
-        "(1,3): expected-doctype-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "comment": true
-        },
-        "tree": [
-          {
-            "comment": "#"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!--#--><html><head></head><body></body></html>",
-        "noQuirksBodyHtml": "<!--#-->"
-      }
-    },
-    {
-      "data": "<?",
-      "errors": [
-        "(1,1): expected-tag-name-but-got-question-mark",
-        "(1,2): expected-doctype-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "comment": true
-        },
-        "tree": [
-          {
-            "comment": "?"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!--?--><html><head></head><body></body></html>",
-        "noQuirksBodyHtml": "<!--?-->"
-      }
-    },
-    {
-      "data": "<?#",
-      "errors": [
-        "(1,1): expected-tag-name-but-got-question-mark",
-        "(1,3): expected-doctype-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "comment": true
-        },
-        "tree": [
-          {
-            "comment": "?#"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!--?#--><html><head></head><body></body></html>",
-        "noQuirksBodyHtml": "<!--?#-->"
-      }
-    },
-    {
-      "data": "<!",
-      "errors": [
-        "(1,2): expected-dashes-or-doctype",
-        "(1,2): expected-doctype-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "comment": true
-        },
-        "tree": [
-          {
-            "comment": ""
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!----><html><head></head><body></body></html>",
-        "noQuirksBodyHtml": "<!---->"
-      }
-    },
-    {
-      "data": "<!#",
-      "errors": [
-        "(1,2): expected-dashes-or-doctype",
-        "(1,3): expected-doctype-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "comment": true
-        },
-        "tree": [
-          {
-            "comment": "#"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!--#--><html><head></head><body></body></html>",
-        "noQuirksBodyHtml": "<!--#-->"
-      }
-    },
-    {
-      "data": "<?COMMENT?>",
-      "errors": [
-        "(1,1): expected-tag-name-but-got-question-mark",
-        "(1,11): expected-doctype-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "comment": true
-        },
-        "tree": [
-          {
-            "comment": "?COMMENT?"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!--?COMMENT?--><html><head></head><body></body></html>",
-        "noQuirksBodyHtml": "<!--?COMMENT?-->"
-      }
-    },
-    {
-      "data": "<!COMMENT>",
-      "errors": [
-        "(1,2): expected-dashes-or-doctype",
-        "(1,10): expected-doctype-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "comment": true
-        },
-        "tree": [
-          {
-            "comment": "COMMENT"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!--COMMENT--><html><head></head><body></body></html>",
-        "noQuirksBodyHtml": "<!--COMMENT-->"
-      }
-    },
-    {
-      "data": "</ COMMENT >",
-      "errors": [
-        "(1,2): expected-closing-tag-but-got-char",
-        "(1,12): expected-doctype-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "comment": true
-        },
-        "tree": [
-          {
-            "comment": " COMMENT "
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!-- COMMENT --><html><head></head><body></body></html>",
-        "noQuirksBodyHtml": "<!-- COMMENT -->"
-      }
-    },
-    {
-      "data": "<?COM--MENT?>",
-      "errors": [
-        "(1,1): expected-tag-name-but-got-question-mark",
-        "(1,13): expected-doctype-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "comment": true
-        },
-        "tree": [
-          {
-            "comment": "?COM--MENT?"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!--?COM--MENT?--><html><head></head><body></body></html>",
-        "noQuirksBodyHtml": "<!--?COM--MENT?-->"
-      }
-    },
-    {
-      "data": "<!COM--MENT>",
-      "errors": [
-        "(1,2): expected-dashes-or-doctype",
-        "(1,12): expected-doctype-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "comment": true
-        },
-        "tree": [
-          {
-            "comment": "COM--MENT"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!--COM--MENT--><html><head></head><body></body></html>",
-        "noQuirksBodyHtml": "<!--COM--MENT-->"
-      }
-    },
-    {
-      "data": "</ COM--MENT >",
-      "errors": [
-        "(1,2): expected-closing-tag-but-got-char",
-        "(1,14): expected-doctype-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "comment": true
-        },
-        "tree": [
-          {
-            "comment": " COM--MENT "
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!-- COM--MENT --><html><head></head><body></body></html>",
-        "noQuirksBodyHtml": "<!-- COM--MENT -->"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><style> EOF",
-      "errors": [
-        "(1,26): expected-named-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "style": true,
-            "body": true
-          },
-          "doctype": true,
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "style",
-                    "children": [
-                      {
-                        "text": " EOF",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head><style> EOF</style></head><body></body></html>",
-        "noQuirksBodyHtml": "<style> EOF</style>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><script> <!-- </script> --> </script> EOF",
-      "errors": [
-        "(1,52): unexpected-end-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "doctype": true,
-          "no_escape": true,
-          "escaped": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": " <!-- ",
-                        "no_escape": true
-                      }
-                    ]
-                  },
-                  {
-                    "text": " "
-                  }
-                ]
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "-->  EOF",
-                    "escaped": true
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head><script> <!-- </script> </head><body>--&gt;  EOF</body></html>",
-        "noQuirksBodyHtml": "<script> <!-- </script> --&gt;  EOF"
-      }
-    },
-    {
-      "data": "<b><p></b>TEST",
-      "errors": [
-        "(1,3): expected-doctype-but-got-start-tag",
-        "(1,10): adoption-agency-1.3"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "b": true,
-            "p": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "b"
-                  },
-                  {
-                    "tag": "p",
-                    "children": [
-                      {
-                        "tag": "b"
-                      },
-                      {
-                        "text": "TEST"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><b></b><p><b></b>TEST</p></body></html>",
-        "noQuirksBodyHtml": "<b></b><p><b></b>TEST</p>"
-      }
-    },
-    {
-      "data": "<p id=a><b><p id=b></b>TEST",
-      "errors": [
-        "(1,8): expected-doctype-but-got-start-tag",
-        "(1,19): unexpected-end-tag",
-        "(1,23): adoption-agency-1.2"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "p": true,
-            "b": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "p",
-                    "attrs": [
-                      {
-                        "name": "id",
-                        "value": "a"
-                      }
-                    ],
-                    "children": [
-                      {
-                        "tag": "b"
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "p",
-                    "attrs": [
-                      {
-                        "name": "id",
-                        "value": "b"
-                      }
-                    ],
-                    "children": [
-                      {
-                        "text": "TEST"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><p id=\"a\"><b></b></p><p id=\"b\">TEST</p></body></html>",
-        "noQuirksBodyHtml": "<p id=\"a\"><b></b></p><p id=\"b\">TEST</p>"
-      }
-    },
-    {
-      "data": "<b id=a><p><b id=b></p></b>TEST",
-      "errors": [
-        "(1,8): expected-doctype-but-got-start-tag",
-        "(1,23): unexpected-end-tag",
-        "(1,27): adoption-agency-1.2",
-        "(1,31): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "b": true,
-            "p": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "b",
-                    "attrs": [
-                      {
-                        "name": "id",
-                        "value": "a"
-                      }
-                    ],
-                    "children": [
-                      {
-                        "tag": "p",
-                        "children": [
-                          {
-                            "tag": "b",
-                            "attrs": [
-                              {
-                                "name": "id",
-                                "value": "b"
-                              }
-                            ]
-                          }
-                        ]
-                      },
-                      {
-                        "text": "TEST"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><b id=\"a\"><p><b id=\"b\"></b></p>TEST</b></body></html>",
-        "noQuirksBodyHtml": "<b id=\"a\"><p><b id=\"b\"></b></p>TEST</b>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><title>U-test</title><body><div><p>Test<u></p></div></body>",
-      "errors": [
-        "(1,61): unexpected-end-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "title": true,
-            "body": true,
-            "div": true,
-            "p": true,
-            "u": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "title",
-                    "children": [
-                      {
-                        "text": "U-test"
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "div",
-                    "children": [
-                      {
-                        "tag": "p",
-                        "children": [
-                          {
-                            "text": "Test"
-                          },
-                          {
-                            "tag": "u"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head><title>U-test</title></head><body><div><p>Test<u></u></p></div></body></html>",
-        "noQuirksBodyHtml": "<title>U-test</title><div><p>Test<u></u></p></div>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><font><table></font></table></font>",
-      "errors": [
-        "(1,35): unexpected-end-tag-implies-table-voodoo",
-        "(1,35): unexpected-end-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "font": true,
-            "table": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "font",
-                    "children": [
-                      {
-                        "tag": "table"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><font><table></table></font></body></html>",
-        "noQuirksBodyHtml": "<font><table></table></font>"
-      }
-    },
-    {
-      "data": "<font><p>hello<b>cruel</font>world",
-      "errors": [
-        "(1,6): expected-doctype-but-got-start-tag",
-        "(1,29): adoption-agency-1.3",
-        "(1,29): adoption-agency-1.3",
-        "(1,34): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "font": true,
-            "p": true,
-            "b": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "font"
-                  },
-                  {
-                    "tag": "p",
-                    "children": [
-                      {
-                        "tag": "font",
-                        "children": [
-                          {
-                            "text": "hello"
-                          },
-                          {
-                            "tag": "b",
-                            "children": [
-                              {
-                                "text": "cruel"
-                              }
-                            ]
-                          }
-                        ]
-                      },
-                      {
-                        "tag": "b",
-                        "children": [
-                          {
-                            "text": "world"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><font></font><p><font>hello<b>cruel</b></font><b>world</b></p></body></html>",
-        "noQuirksBodyHtml": "<font></font><p><font>hello<b>cruel</b></font><b>world</b></p>"
-      }
-    },
-    {
-      "data": "<b>Test</i>Test",
-      "errors": [
-        "(1,3): expected-doctype-but-got-start-tag",
-        "(1,11): unexpected-end-tag",
-        "(1,15): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "b": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "b",
-                    "children": [
-                      {
-                        "text": "TestTest"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><b>TestTest</b></body></html>",
-        "noQuirksBodyHtml": "<b>TestTest</b>"
-      }
-    },
-    {
-      "data": "<b>A<cite>B<div>C",
-      "errors": [
-        "(1,3): expected-doctype-but-got-start-tag",
-        "(1,17): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "b": true,
-            "cite": true,
-            "div": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "b",
-                    "children": [
-                      {
-                        "text": "A"
-                      },
-                      {
-                        "tag": "cite",
-                        "children": [
-                          {
-                            "text": "B"
-                          },
-                          {
-                            "tag": "div",
-                            "children": [
-                              {
-                                "text": "C"
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><b>A<cite>B<div>C</div></cite></b></body></html>",
-        "noQuirksBodyHtml": "<b>A<cite>B<div>C</div></cite></b>"
-      }
-    },
-    {
-      "data": "<b>A<cite>B<div>C</cite>D",
-      "errors": [
-        "(1,3): expected-doctype-but-got-start-tag",
-        "(1,24): unexpected-end-tag",
-        "(1,25): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "b": true,
-            "cite": true,
-            "div": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "b",
-                    "children": [
-                      {
-                        "text": "A"
-                      },
-                      {
-                        "tag": "cite",
-                        "children": [
-                          {
-                            "text": "B"
-                          },
-                          {
-                            "tag": "div",
-                            "children": [
-                              {
-                                "text": "CD"
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><b>A<cite>B<div>CD</div></cite></b></body></html>",
-        "noQuirksBodyHtml": "<b>A<cite>B<div>CD</div></cite></b>"
-      }
-    },
-    {
-      "data": "<b>A<cite>B<div>C</b>D",
-      "errors": [
-        "(1,3): expected-doctype-but-got-start-tag",
-        "(1,21): adoption-agency-1.3",
-        "(1,22): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "b": true,
-            "cite": true,
-            "div": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "b",
-                    "children": [
-                      {
-                        "text": "A"
-                      },
-                      {
-                        "tag": "cite",
-                        "children": [
-                          {
-                            "text": "B"
-                          }
-                        ]
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "div",
-                    "children": [
-                      {
-                        "tag": "b",
-                        "children": [
-                          {
-                            "text": "C"
-                          }
-                        ]
-                      },
-                      {
-                        "text": "D"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><b>A<cite>B</cite></b><div><b>C</b>D</div></body></html>",
-        "noQuirksBodyHtml": "<b>A<cite>B</cite></b><div><b>C</b>D</div>"
-      }
-    },
-    {
-      "data": "",
-      "errors": [
-        "(1,0): expected-doctype-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body></body></html>",
-        "noQuirksBodyHtml": ""
-      }
-    },
-    {
-      "data": "<DIV>",
-      "errors": [
-        "(1,5): expected-doctype-but-got-start-tag",
-        "(1,5): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "div": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "div"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><div></div></body></html>",
-        "noQuirksBodyHtml": "<div></div>"
-      }
-    },
-    {
-      "data": "<DIV> abc",
-      "errors": [
-        "(1,5): expected-doctype-but-got-start-tag",
-        "(1,9): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "div": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "div",
-                    "children": [
-                      {
-                        "text": " abc"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><div> abc</div></body></html>",
-        "noQuirksBodyHtml": "<div> abc</div>"
-      }
-    },
-    {
-      "data": "<DIV> abc <B>",
-      "errors": [
-        "(1,5): expected-doctype-but-got-start-tag",
-        "(1,13): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "div": true,
-            "b": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "div",
-                    "children": [
-                      {
-                        "text": " abc "
-                      },
-                      {
-                        "tag": "b"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><div> abc <b></b></div></body></html>",
-        "noQuirksBodyHtml": "<div> abc <b></b></div>"
-      }
-    },
-    {
-      "data": "<DIV> abc <B> def",
-      "errors": [
-        "(1,5): expected-doctype-but-got-start-tag",
-        "(1,17): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "div": true,
-            "b": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "div",
-                    "children": [
-                      {
-                        "text": " abc "
-                      },
-                      {
-                        "tag": "b",
-                        "children": [
-                          {
-                            "text": " def"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><div> abc <b> def</b></div></body></html>",
-        "noQuirksBodyHtml": "<div> abc <b> def</b></div>"
-      }
-    },
-    {
-      "data": "<DIV> abc <B> def <I>",
-      "errors": [
-        "(1,5): expected-doctype-but-got-start-tag",
-        "(1,21): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "div": true,
-            "b": true,
-            "i": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "div",
-                    "children": [
-                      {
-                        "text": " abc "
-                      },
-                      {
-                        "tag": "b",
-                        "children": [
-                          {
-                            "text": " def "
-                          },
-                          {
-                            "tag": "i"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><div> abc <b> def <i></i></b></div></body></html>",
-        "noQuirksBodyHtml": "<div> abc <b> def <i></i></b></div>"
-      }
-    },
-    {
-      "data": "<DIV> abc <B> def <I> ghi",
-      "errors": [
-        "(1,5): expected-doctype-but-got-start-tag",
-        "(1,25): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "div": true,
-            "b": true,
-            "i": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "div",
-                    "children": [
-                      {
-                        "text": " abc "
-                      },
-                      {
-                        "tag": "b",
-                        "children": [
-                          {
-                            "text": " def "
-                          },
-                          {
-                            "tag": "i",
-                            "children": [
-                              {
-                                "text": " ghi"
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><div> abc <b> def <i> ghi</i></b></div></body></html>",
-        "noQuirksBodyHtml": "<div> abc <b> def <i> ghi</i></b></div>"
-      }
-    },
-    {
-      "data": "<DIV> abc <B> def <I> ghi <P>",
-      "errors": [
-        "(1,5): expected-doctype-but-got-start-tag",
-        "(1,29): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "div": true,
-            "b": true,
-            "i": true,
-            "p": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "div",
-                    "children": [
-                      {
-                        "text": " abc "
-                      },
-                      {
-                        "tag": "b",
-                        "children": [
-                          {
-                            "text": " def "
-                          },
-                          {
-                            "tag": "i",
-                            "children": [
-                              {
-                                "text": " ghi "
-                              },
-                              {
-                                "tag": "p"
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><div> abc <b> def <i> ghi <p></p></i></b></div></body></html>",
-        "noQuirksBodyHtml": "<div> abc <b> def <i> ghi <p></p></i></b></div>"
-      }
-    },
-    {
-      "data": "<DIV> abc <B> def <I> ghi <P> jkl",
-      "errors": [
-        "(1,5): expected-doctype-but-got-start-tag",
-        "(1,33): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "div": true,
-            "b": true,
-            "i": true,
-            "p": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "div",
-                    "children": [
-                      {
-                        "text": " abc "
-                      },
-                      {
-                        "tag": "b",
-                        "children": [
-                          {
-                            "text": " def "
-                          },
-                          {
-                            "tag": "i",
-                            "children": [
-                              {
-                                "text": " ghi "
-                              },
-                              {
-                                "tag": "p",
-                                "children": [
-                                  {
-                                    "text": " jkl"
-                                  }
-                                ]
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><div> abc <b> def <i> ghi <p> jkl</p></i></b></div></body></html>",
-        "noQuirksBodyHtml": "<div> abc <b> def <i> ghi <p> jkl</p></i></b></div>"
-      }
-    },
-    {
-      "data": "<DIV> abc <B> def <I> ghi <P> jkl </B>",
-      "errors": [
-        "(1,5): expected-doctype-but-got-start-tag",
-        "(1,38): adoption-agency-1.3",
-        "(1,38): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "div": true,
-            "b": true,
-            "i": true,
-            "p": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "div",
-                    "children": [
-                      {
-                        "text": " abc "
-                      },
-                      {
-                        "tag": "b",
-                        "children": [
-                          {
-                            "text": " def "
-                          },
-                          {
-                            "tag": "i",
-                            "children": [
-                              {
-                                "text": " ghi "
-                              }
-                            ]
-                          }
-                        ]
-                      },
-                      {
-                        "tag": "i",
-                        "children": [
-                          {
-                            "tag": "p",
-                            "children": [
-                              {
-                                "tag": "b",
-                                "children": [
-                                  {
-                                    "text": " jkl "
-                                  }
-                                ]
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><div> abc <b> def <i> ghi </i></b><i><p><b> jkl </b></p></i></div></body></html>",
-        "noQuirksBodyHtml": "<div> abc <b> def <i> ghi </i></b><i><p><b> jkl </b></p></i></div>"
-      }
-    },
-    {
-      "data": "<DIV> abc <B> def <I> ghi <P> jkl </B> mno",
-      "errors": [
-        "(1,5): expected-doctype-but-got-start-tag",
-        "(1,38): adoption-agency-1.3",
-        "(1,42): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "div": true,
-            "b": true,
-            "i": true,
-            "p": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "div",
-                    "children": [
-                      {
-                        "text": " abc "
-                      },
-                      {
-                        "tag": "b",
-                        "children": [
-                          {
-                            "text": " def "
-                          },
-                          {
-                            "tag": "i",
-                            "children": [
-                              {
-                                "text": " ghi "
-                              }
-                            ]
-                          }
-                        ]
-                      },
-                      {
-                        "tag": "i",
-                        "children": [
-                          {
-                            "tag": "p",
-                            "children": [
-                              {
-                                "tag": "b",
-                                "children": [
-                                  {
-                                    "text": " jkl "
-                                  }
-                                ]
-                              },
-                              {
-                                "text": " mno"
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><div> abc <b> def <i> ghi </i></b><i><p><b> jkl </b> mno</p></i></div></body></html>",
-        "noQuirksBodyHtml": "<div> abc <b> def <i> ghi </i></b><i><p><b> jkl </b> mno</p></i></div>"
-      }
-    },
-    {
-      "data": "<DIV> abc <B> def <I> ghi <P> jkl </B> mno </I>",
-      "errors": [
-        "(1,5): expected-doctype-but-got-start-tag",
-        "(1,38): adoption-agency-1.3",
-        "(1,47): adoption-agency-1.3",
-        "(1,47): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "div": true,
-            "b": true,
-            "i": true,
-            "p": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "div",
-                    "children": [
-                      {
-                        "text": " abc "
-                      },
-                      {
-                        "tag": "b",
-                        "children": [
-                          {
-                            "text": " def "
-                          },
-                          {
-                            "tag": "i",
-                            "children": [
-                              {
-                                "text": " ghi "
-                              }
-                            ]
-                          }
-                        ]
-                      },
-                      {
-                        "tag": "i"
-                      },
-                      {
-                        "tag": "p",
-                        "children": [
-                          {
-                            "tag": "i",
-                            "children": [
-                              {
-                                "tag": "b",
-                                "children": [
-                                  {
-                                    "text": " jkl "
-                                  }
-                                ]
-                              },
-                              {
-                                "text": " mno "
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><div> abc <b> def <i> ghi </i></b><i></i><p><i><b> jkl </b> mno </i></p></div></body></html>",
-        "noQuirksBodyHtml": "<div> abc <b> def <i> ghi </i></b><i></i><p><i><b> jkl </b> mno </i></p></div>"
-      }
-    },
-    {
-      "data": "<DIV> abc <B> def <I> ghi <P> jkl </B> mno </I> pqr",
-      "errors": [
-        "(1,5): expected-doctype-but-got-start-tag",
-        "(1,38): adoption-agency-1.3",
-        "(1,47): adoption-agency-1.3",
-        "(1,51): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "div": true,
-            "b": true,
-            "i": true,
-            "p": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "div",
-                    "children": [
-                      {
-                        "text": " abc "
-                      },
-                      {
-                        "tag": "b",
-                        "children": [
-                          {
-                            "text": " def "
-                          },
-                          {
-                            "tag": "i",
-                            "children": [
-                              {
-                                "text": " ghi "
-                              }
-                            ]
-                          }
-                        ]
-                      },
-                      {
-                        "tag": "i"
-                      },
-                      {
-                        "tag": "p",
-                        "children": [
-                          {
-                            "tag": "i",
-                            "children": [
-                              {
-                                "tag": "b",
-                                "children": [
-                                  {
-                                    "text": " jkl "
-                                  }
-                                ]
-                              },
-                              {
-                                "text": " mno "
-                              }
-                            ]
-                          },
-                          {
-                            "text": " pqr"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><div> abc <b> def <i> ghi </i></b><i></i><p><i><b> jkl </b> mno </i> pqr</p></div></body></html>",
-        "noQuirksBodyHtml": "<div> abc <b> def <i> ghi </i></b><i></i><p><i><b> jkl </b> mno </i> pqr</p></div>"
-      }
-    },
-    {
-      "data": "<DIV> abc <B> def <I> ghi <P> jkl </B> mno </I> pqr </P>",
-      "errors": [
-        "(1,5): expected-doctype-but-got-start-tag",
-        "(1,38): adoption-agency-1.3",
-        "(1,47): adoption-agency-1.3",
-        "(1,56): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "div": true,
-            "b": true,
-            "i": true,
-            "p": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "div",
-                    "children": [
-                      {
-                        "text": " abc "
-                      },
-                      {
-                        "tag": "b",
-                        "children": [
-                          {
-                            "text": " def "
-                          },
-                          {
-                            "tag": "i",
-                            "children": [
-                              {
-                                "text": " ghi "
-                              }
-                            ]
-                          }
-                        ]
-                      },
-                      {
-                        "tag": "i"
-                      },
-                      {
-                        "tag": "p",
-                        "children": [
-                          {
-                            "tag": "i",
-                            "children": [
-                              {
-                                "tag": "b",
-                                "children": [
-                                  {
-                                    "text": " jkl "
-                                  }
-                                ]
-                              },
-                              {
-                                "text": " mno "
-                              }
-                            ]
-                          },
-                          {
-                            "text": " pqr "
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><div> abc <b> def <i> ghi </i></b><i></i><p><i><b> jkl </b> mno </i> pqr </p></div></body></html>",
-        "noQuirksBodyHtml": "<div> abc <b> def <i> ghi </i></b><i></i><p><i><b> jkl </b> mno </i> pqr </p></div>"
-      }
-    },
-    {
-      "data": "<DIV> abc <B> def <I> ghi <P> jkl </B> mno </I> pqr </P> stu",
-      "errors": [
-        "(1,5): expected-doctype-but-got-start-tag",
-        "(1,38): adoption-agency-1.3",
-        "(1,47): adoption-agency-1.3",
-        "(1,60): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "div": true,
-            "b": true,
-            "i": true,
-            "p": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "div",
-                    "children": [
-                      {
-                        "text": " abc "
-                      },
-                      {
-                        "tag": "b",
-                        "children": [
-                          {
-                            "text": " def "
-                          },
-                          {
-                            "tag": "i",
-                            "children": [
-                              {
-                                "text": " ghi "
-                              }
-                            ]
-                          }
-                        ]
-                      },
-                      {
-                        "tag": "i"
-                      },
-                      {
-                        "tag": "p",
-                        "children": [
-                          {
-                            "tag": "i",
-                            "children": [
-                              {
-                                "tag": "b",
-                                "children": [
-                                  {
-                                    "text": " jkl "
-                                  }
-                                ]
-                              },
-                              {
-                                "text": " mno "
-                              }
-                            ]
-                          },
-                          {
-                            "text": " pqr "
-                          }
-                        ]
-                      },
-                      {
-                        "text": " stu"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><div> abc <b> def <i> ghi </i></b><i></i><p><i><b> jkl </b> mno </i> pqr </p> stu</div></body></html>",
-        "noQuirksBodyHtml": "<div> abc <b> def <i> ghi </i></b><i></i><p><i><b> jkl </b> mno </i> pqr </p> stu</div>"
-      }
-    },
-    {
-      "data": "<test attribute---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------->",
-      "errors": [
-        "(1,1040): expected-doctype-but-got-start-tag",
-        "(1,1040): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "test": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "test",
-                    "attrs": [
-                      {
-                        "name": "attribute----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------",
-                        "value": ""
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><test attribute----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------=\"\"></test></body></html>",
-        "noQuirksBodyHtml": "<test attribute----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------=\"\"></test>"
-      }
-    },
-    {
-      "data": "<a href=\"blah\">aba<table><a href=\"foo\">br<tr><td></td></tr>x</table>aoe",
-      "errors": [
-        "(1,15): expected-doctype-but-got-start-tag",
-        "(1,39): unexpected-start-tag-implies-table-voodoo",
-        "(1,39): unexpected-start-tag-implies-end-tag",
-        "(1,39): unexpected-end-tag",
-        "(1,45): foster-parenting-character-in-table",
-        "(1,45): foster-parenting-character-in-table",
-        "(1,68): foster-parenting-character-in-table",
-        "(1,71): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "a": true,
-            "table": true,
-            "tbody": true,
-            "tr": true,
-            "td": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "a",
-                    "attrs": [
-                      {
-                        "name": "href",
-                        "value": "blah"
-                      }
-                    ],
-                    "children": [
-                      {
-                        "text": "aba"
-                      },
-                      {
-                        "tag": "a",
-                        "attrs": [
-                          {
-                            "name": "href",
-                            "value": "foo"
-                          }
-                        ],
-                        "children": [
-                          {
-                            "text": "br"
-                          }
-                        ]
-                      },
-                      {
-                        "tag": "a",
-                        "attrs": [
-                          {
-                            "name": "href",
-                            "value": "foo"
-                          }
-                        ],
-                        "children": [
-                          {
-                            "text": "x"
-                          }
-                        ]
-                      },
-                      {
-                        "tag": "table",
-                        "children": [
-                          {
-                            "tag": "tbody",
-                            "children": [
-                              {
-                                "tag": "tr",
-                                "children": [
-                                  {
-                                    "tag": "td"
-                                  }
-                                ]
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "a",
-                    "attrs": [
-                      {
-                        "name": "href",
-                        "value": "foo"
-                      }
-                    ],
-                    "children": [
-                      {
-                        "text": "aoe"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><a href=\"blah\">aba<a href=\"foo\">br</a><a href=\"foo\">x</a><table><tbody><tr><td></td></tr></tbody></table></a><a href=\"foo\">aoe</a></body></html>",
-        "noQuirksBodyHtml": "<a href=\"blah\">aba<a href=\"foo\">br</a><a href=\"foo\">x</a><table><tbody><tr><td></td></tr></tbody></table></a><a href=\"foo\">aoe</a>"
-      }
-    },
-    {
-      "data": "<a href=\"blah\">aba<table><tr><td><a href=\"foo\">br</td></tr>x</table>aoe",
-      "errors": [
-        "(1,15): expected-doctype-but-got-start-tag",
-        "(1,54): unexpected-cell-end-tag",
-        "(1,68): unexpected text in table",
-        "(1,71): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "a": true,
-            "table": true,
-            "tbody": true,
-            "tr": true,
-            "td": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "a",
-                    "attrs": [
-                      {
-                        "name": "href",
-                        "value": "blah"
-                      }
-                    ],
-                    "children": [
-                      {
-                        "text": "abax"
-                      },
-                      {
-                        "tag": "table",
-                        "children": [
-                          {
-                            "tag": "tbody",
-                            "children": [
-                              {
-                                "tag": "tr",
-                                "children": [
-                                  {
-                                    "tag": "td",
-                                    "children": [
-                                      {
-                                        "tag": "a",
-                                        "attrs": [
-                                          {
-                                            "name": "href",
-                                            "value": "foo"
-                                          }
-                                        ],
-                                        "children": [
-                                          {
-                                            "text": "br"
-                                          }
-                                        ]
-                                      }
-                                    ]
-                                  }
-                                ]
-                              }
-                            ]
-                          }
-                        ]
-                      },
-                      {
-                        "text": "aoe"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><a href=\"blah\">abax<table><tbody><tr><td><a href=\"foo\">br</a></td></tr></tbody></table>aoe</a></body></html>",
-        "noQuirksBodyHtml": "<a href=\"blah\">abax<table><tbody><tr><td><a href=\"foo\">br</a></td></tr></tbody></table>aoe</a>"
-      }
-    },
-    {
-      "data": "<table><a href=\"blah\">aba<tr><td><a href=\"foo\">br</td></tr>x</table>aoe",
-      "errors": [
-        "(1,7): expected-doctype-but-got-start-tag",
-        "(1,22): unexpected-start-tag-implies-table-voodoo",
-        "(1,29): foster-parenting-character-in-table",
-        "(1,29): foster-parenting-character-in-table",
-        "(1,29): foster-parenting-character-in-table",
-        "(1,54): unexpected-cell-end-tag",
-        "(1,68): foster-parenting-character-in-table",
-        "(1,71): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "a": true,
-            "table": true,
-            "tbody": true,
-            "tr": true,
-            "td": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "a",
-                    "attrs": [
-                      {
-                        "name": "href",
-                        "value": "blah"
-                      }
-                    ],
-                    "children": [
-                      {
-                        "text": "aba"
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "a",
-                    "attrs": [
-                      {
-                        "name": "href",
-                        "value": "blah"
-                      }
-                    ],
-                    "children": [
-                      {
-                        "text": "x"
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "tbody",
-                        "children": [
-                          {
-                            "tag": "tr",
-                            "children": [
-                              {
-                                "tag": "td",
-                                "children": [
-                                  {
-                                    "tag": "a",
-                                    "attrs": [
-                                      {
-                                        "name": "href",
-                                        "value": "foo"
-                                      }
-                                    ],
-                                    "children": [
-                                      {
-                                        "text": "br"
-                                      }
-                                    ]
-                                  }
-                                ]
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "a",
-                    "attrs": [
-                      {
-                        "name": "href",
-                        "value": "blah"
-                      }
-                    ],
-                    "children": [
-                      {
-                        "text": "aoe"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><a href=\"blah\">aba</a><a href=\"blah\">x</a><table><tbody><tr><td><a href=\"foo\">br</a></td></tr></tbody></table><a href=\"blah\">aoe</a></body></html>",
-        "noQuirksBodyHtml": "<a href=\"blah\">aba</a><a href=\"blah\">x</a><table><tbody><tr><td><a href=\"foo\">br</a></td></tr></tbody></table><a href=\"blah\">aoe</a>"
-      }
-    },
-    {
-      "data": "<a href=a>aa<marquee>aa<a href=b>bb</marquee>aa",
-      "errors": [
-        "(1,10): expected-doctype-but-got-start-tag",
-        "(1,45): end-tag-too-early",
-        "(1,47): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "a": true,
-            "marquee": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "a",
-                    "attrs": [
-                      {
-                        "name": "href",
-                        "value": "a"
-                      }
-                    ],
-                    "children": [
-                      {
-                        "text": "aa"
-                      },
-                      {
-                        "tag": "marquee",
-                        "children": [
-                          {
-                            "text": "aa"
-                          },
-                          {
-                            "tag": "a",
-                            "attrs": [
-                              {
-                                "name": "href",
-                                "value": "b"
-                              }
-                            ],
-                            "children": [
-                              {
-                                "text": "bb"
-                              }
-                            ]
-                          }
-                        ]
-                      },
-                      {
-                        "text": "aa"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><a href=\"a\">aa<marquee>aa<a href=\"b\">bb</a></marquee>aa</a></body></html>",
-        "noQuirksBodyHtml": "<a href=\"a\">aa<marquee>aa<a href=\"b\">bb</a></marquee>aa</a>"
-      }
-    },
-    {
-      "data": "<wbr><strike><code></strike><code><strike></code>",
-      "errors": [
-        "(1,5): expected-doctype-but-got-start-tag",
-        "(1,28): adoption-agency-1.3",
-        "(1,49): adoption-agency-1.3",
-        "(1,49): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "wbr": true,
-            "strike": true,
-            "code": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "wbr"
-                  },
-                  {
-                    "tag": "strike",
-                    "children": [
-                      {
-                        "tag": "code"
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "code",
-                    "children": [
-                      {
-                        "tag": "code",
-                        "children": [
-                          {
-                            "tag": "strike"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><wbr><strike><code></code></strike><code><code><strike></strike></code></code></body></html>",
-        "noQuirksBodyHtml": "<wbr><strike><code></code></strike><code><code><strike></strike></code></code>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><spacer>foo",
-      "errors": [
-        "(1,26): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "spacer": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "spacer",
-                    "children": [
-                      {
-                        "text": "foo"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><spacer>foo</spacer></body></html>",
-        "noQuirksBodyHtml": "<spacer>foo</spacer>"
-      }
-    },
-    {
-      "data": "<title><meta></title><link><title><meta></title>",
-      "errors": [
-        "(1,7): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "title": true,
-            "link": true,
-            "body": true
-          },
-          "escaped": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "title",
-                    "children": [
-                      {
-                        "text": "<meta>",
-                        "escaped": true
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "link"
-                  },
-                  {
-                    "tag": "title",
-                    "children": [
-                      {
-                        "text": "<meta>",
-                        "escaped": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><title>&lt;meta&gt;</title><link><title>&lt;meta&gt;</title></head><body></body></html>",
-        "noQuirksBodyHtml": "<title>&lt;meta&gt;</title><link><title>&lt;meta&gt;</title>"
-      }
-    },
-    {
-      "data": "<style><!--</style><meta><script>--><link></script>",
-      "errors": [
-        "(1,7): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "style": true,
-            "meta": true,
-            "script": true,
-            "body": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "style",
-                    "children": [
-                      {
-                        "text": "<!--",
-                        "no_escape": true
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "meta"
-                  },
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "--><link>",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><style><!--</style><meta><script>--><link></script></head><body></body></html>",
-        "noQuirksBodyHtml": "<style><!--</style><meta><script>--><link></script>"
-      }
-    },
-    {
-      "data": "<head><meta></head><link>",
-      "errors": [
-        "(1,6): expected-doctype-but-got-start-tag",
-        "(1,25): unexpected-start-tag-out-of-my-head"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "meta": true,
-            "link": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "meta"
-                  },
-                  {
-                    "tag": "link"
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><meta><link></head><body></body></html>",
-        "noQuirksBodyHtml": "<meta><link>"
-      }
-    },
-    {
-      "data": "<table><tr><tr><td><td><span><th><span>X</table>",
-      "errors": [
-        "(1,7): expected-doctype-but-got-start-tag",
-        "(1,33): unexpected-cell-end-tag",
-        "(1,48): unexpected-cell-end-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true,
-            "tbody": true,
-            "tr": true,
-            "td": true,
-            "span": true,
-            "th": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "tbody",
-                        "children": [
-                          {
-                            "tag": "tr"
-                          },
-                          {
-                            "tag": "tr",
-                            "children": [
-                              {
-                                "tag": "td"
-                              },
-                              {
-                                "tag": "td",
-                                "children": [
-                                  {
-                                    "tag": "span"
-                                  }
-                                ]
-                              },
-                              {
-                                "tag": "th",
-                                "children": [
-                                  {
-                                    "tag": "span",
-                                    "children": [
-                                      {
-                                        "text": "X"
-                                      }
-                                    ]
-                                  }
-                                ]
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><table><tbody><tr></tr><tr><td></td><td><span></span></td><th><span>X</span></th></tr></tbody></table></body></html>",
-        "noQuirksBodyHtml": "<table><tbody><tr></tr><tr><td></td><td><span></span></td><th><span>X</span></th></tr></tbody></table>"
-      }
-    },
-    {
-      "data": "<body><body><base><link><meta><title><p></title><body><p></body>",
-      "errors": [
-        "(1,6): expected-doctype-but-got-start-tag",
-        "(1,12): unexpected-start-tag",
-        "(1,54): unexpected-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "base": true,
-            "link": true,
-            "meta": true,
-            "title": true,
-            "p": true
-          },
-          "escaped": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "base"
-                  },
-                  {
-                    "tag": "link"
-                  },
-                  {
-                    "tag": "meta"
-                  },
-                  {
-                    "tag": "title",
-                    "children": [
-                      {
-                        "text": "<p>",
-                        "escaped": true
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "p"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><base><link><meta><title>&lt;p&gt;</title><p></p></body></html>",
-        "noQuirksBodyHtml": "<base><link><meta><title>&lt;p&gt;</title><p></p>"
-      }
-    },
-    {
-      "data": "<textarea><p></textarea>",
-      "errors": [
-        "(1,10): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "textarea": true
-          },
-          "escaped": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "textarea",
-                    "children": [
-                      {
-                        "text": "<p>",
-                        "escaped": true
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><textarea>&lt;p&gt;</textarea></body></html>",
-        "noQuirksBodyHtml": "<textarea>&lt;p&gt;</textarea>"
-      }
-    },
-    {
-      "data": "<p><image></p>",
-      "errors": [
-        "(1,3): expected-doctype-but-got-start-tag",
-        "(1,10): unexpected-start-tag-treated-as"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "p": true,
-            "img": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "p",
-                    "children": [
-                      {
-                        "tag": "img"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><p><img></p></body></html>",
-        "noQuirksBodyHtml": "<p><img></p>"
-      }
-    },
-    {
-      "data": "<a><table><a></table><p><a><div><a>",
-      "errors": [
-        "(1,3): expected-doctype-but-got-start-tag",
-        "(1,13): unexpected-start-tag-implies-table-voodoo",
-        "(1,13): unexpected-start-tag-implies-end-tag",
-        "(1,13): adoption-agency-1.3",
-        "(1,27): unexpected-start-tag-implies-end-tag",
-        "(1,27): adoption-agency-1.2",
-        "(1,32): unexpected-end-tag",
-        "(1,35): unexpected-start-tag-implies-end-tag",
-        "(1,35): adoption-agency-1.2",
-        "(1,35): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "a": true,
-            "table": true,
-            "p": true,
-            "div": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "a",
-                    "children": [
-                      {
-                        "tag": "a"
-                      },
-                      {
-                        "tag": "table"
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "p",
-                    "children": [
-                      {
-                        "tag": "a"
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "div",
-                    "children": [
-                      {
-                        "tag": "a"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><a><a></a><table></table></a><p><a></a></p><div><a></a></div></body></html>",
-        "noQuirksBodyHtml": "<a><a></a><table></table></a><p><a></a></p><div><a></a></div>"
-      }
-    },
-    {
-      "data": "<head></p><meta><p>",
-      "errors": [
-        "(1,6): expected-doctype-but-got-start-tag",
-        "(1,10): unexpected-end-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "meta": true,
-            "body": true,
-            "p": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "meta"
-                  }
-                ]
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "p"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><meta></head><body><p></p></body></html>",
-        "noQuirksBodyHtml": "<p></p><meta><p></p>"
-      }
-    },
-    {
-      "data": "<head></html><meta><p>",
-      "errors": [
-        "(1,6): expected-doctype-but-got-start-tag",
-        "(1,19): expected-eof-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "meta": true,
-            "p": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "meta"
-                  },
-                  {
-                    "tag": "p"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><meta><p></p></body></html>",
-        "noQuirksBodyHtml": "<meta><p></p>"
-      }
-    },
-    {
-      "data": "<b><table><td><i></table>",
-      "errors": [
-        "(1,3): expected-doctype-but-got-start-tag",
-        "(1,14): unexpected-cell-in-table-body",
-        "(1,25): unexpected-cell-end-tag",
-        "(1,25): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "b": true,
-            "table": true,
-            "tbody": true,
-            "tr": true,
-            "td": true,
-            "i": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "b",
-                    "children": [
-                      {
-                        "tag": "table",
-                        "children": [
-                          {
-                            "tag": "tbody",
-                            "children": [
-                              {
-                                "tag": "tr",
-                                "children": [
-                                  {
-                                    "tag": "td",
-                                    "children": [
-                                      {
-                                        "tag": "i"
-                                      }
-                                    ]
-                                  }
-                                ]
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><b><table><tbody><tr><td><i></i></td></tr></tbody></table></b></body></html>",
-        "noQuirksBodyHtml": "<b><table><tbody><tr><td><i></i></td></tr></tbody></table></b>"
-      }
-    },
-    {
-      "data": "<b><table><td></b><i></table>",
-      "errors": [
-        "(1,3): expected-doctype-but-got-start-tag",
-        "(1,14): unexpected-cell-in-table-body",
-        "(1,18): unexpected-end-tag",
-        "(1,29): unexpected-cell-end-tag",
-        "(1,29): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "b": true,
-            "table": true,
-            "tbody": true,
-            "tr": true,
-            "td": true,
-            "i": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "b",
-                    "children": [
-                      {
-                        "tag": "table",
-                        "children": [
-                          {
-                            "tag": "tbody",
-                            "children": [
-                              {
-                                "tag": "tr",
-                                "children": [
-                                  {
-                                    "tag": "td",
-                                    "children": [
-                                      {
-                                        "tag": "i"
-                                      }
-                                    ]
-                                  }
-                                ]
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><b><table><tbody><tr><td><i></i></td></tr></tbody></table></b></body></html>",
-        "noQuirksBodyHtml": "<b><table><tbody><tr><td><i></i></td></tr></tbody></table></b>"
-      }
-    },
-    {
-      "data": "<h1><h2>",
-      "errors": [
-        "(1,4): expected-doctype-but-got-start-tag",
-        "(1,8): unexpected-start-tag",
-        "(1,8): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "h1": true,
-            "h2": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "h1"
-                  },
-                  {
-                    "tag": "h2"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><h1></h1><h2></h2></body></html>",
-        "noQuirksBodyHtml": "<h1></h1><h2></h2>"
-      }
-    },
-    {
-      "data": "<a><p><a></a></p></a>",
-      "errors": [
-        "(1,3): expected-doctype-but-got-start-tag",
-        "(1,9): unexpected-start-tag-implies-end-tag",
-        "(1,9): adoption-agency-1.3",
-        "(1,21): unexpected-end-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "a": true,
-            "p": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "a"
-                  },
-                  {
-                    "tag": "p",
-                    "children": [
-                      {
-                        "tag": "a"
-                      },
-                      {
-                        "tag": "a"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><a></a><p><a></a><a></a></p></body></html>",
-        "noQuirksBodyHtml": "<a></a><p><a></a><a></a></p>"
-      }
-    },
-    {
-      "data": "<b><button></b></button></b>",
-      "errors": [
-        "(1,3): expected-doctype-but-got-start-tag",
-        "(1,15): adoption-agency-1.3",
-        "(1,28): unexpected-end-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "b": true,
-            "button": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "b"
-                  },
-                  {
-                    "tag": "button",
-                    "children": [
-                      {
-                        "tag": "b"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><b></b><button><b></b></button></body></html>",
-        "noQuirksBodyHtml": "<b></b><button><b></b></button>"
-      }
-    },
-    {
-      "data": "<p><b><div><marquee></p></b></div>",
-      "errors": [
-        "(1,3): expected-doctype-but-got-start-tag",
-        "(1,11): unexpected-end-tag",
-        "(1,24): unexpected-end-tag",
-        "(1,28): unexpected-end-tag",
-        "(1,34): end-tag-too-early",
-        "(1,34): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "p": true,
-            "b": true,
-            "div": true,
-            "marquee": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "p",
-                    "children": [
-                      {
-                        "tag": "b"
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "div",
-                    "children": [
-                      {
-                        "tag": "b",
-                        "children": [
-                          {
-                            "tag": "marquee",
-                            "children": [
-                              {
-                                "tag": "p"
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><p><b></b></p><div><b><marquee><p></p></marquee></b></div></body></html>",
-        "noQuirksBodyHtml": "<p><b></b></p><div><b><marquee><p></p></marquee></b></div>"
-      }
-    },
-    {
-      "data": "<script></script></div><title></title><p><p>",
-      "errors": [
-        "(1,8): expected-doctype-but-got-start-tag",
-        "(1,23): unexpected-end-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "title": true,
-            "body": true,
-            "p": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script"
-                  },
-                  {
-                    "tag": "title"
-                  }
-                ]
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "p"
-                  },
-                  {
-                    "tag": "p"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><script></script><title></title></head><body><p></p><p></p></body></html>",
-        "noQuirksBodyHtml": "<script></script><title></title><p></p><p></p>"
-      }
-    },
-    {
-      "data": "<p><hr></p>",
-      "errors": [
-        "(1,3): expected-doctype-but-got-start-tag",
-        "(1,11): unexpected-end-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "p": true,
-            "hr": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "p"
-                  },
-                  {
-                    "tag": "hr"
-                  },
-                  {
-                    "tag": "p"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><p></p><hr><p></p></body></html>",
-        "noQuirksBodyHtml": "<p></p><hr><p></p>"
-      }
-    },
-    {
-      "data": "<select><b><option><select><option></b></select>",
-      "errors": [
-        "(1,8): expected-doctype-but-got-start-tag",
-        "(1,11): unexpected-start-tag-in-select",
-        "(1,27): unexpected-select-in-select",
-        "(1,39): unexpected-end-tag",
-        "(1,48): unexpected-end-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "select": true,
-            "option": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "select",
-                    "children": [
-                      {
-                        "tag": "option"
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "option"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><select><option></option></select><option></option></body></html>",
-        "noQuirksBodyHtml": "<select><option></option></select><option></option>"
-      }
-    },
-    {
-      "data": "<html><head><title></title><body></body></html>",
-      "errors": [
-        "(1,6): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "title": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "title"
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><title></title></head><body></body></html>",
-        "noQuirksBodyHtml": "<title></title>"
-      }
-    },
-    {
-      "data": "<a><table><td><a><table></table><a></tr><a></table><a>",
-      "errors": [
-        "(1,3): expected-doctype-but-got-start-tag",
-        "(1,14): unexpected-cell-in-table-body",
-        "(1,35): unexpected-start-tag-implies-end-tag",
-        "(1,40): unexpected-cell-end-tag",
-        "(1,43): unexpected-start-tag-implies-table-voodoo",
-        "(1,43): unexpected-start-tag-implies-end-tag",
-        "(1,43): unexpected-end-tag",
-        "(1,54): unexpected-start-tag-implies-end-tag",
-        "(1,54): adoption-agency-1.2",
-        "(1,54): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "a": true,
-            "table": true,
-            "tbody": true,
-            "tr": true,
-            "td": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "a",
-                    "children": [
-                      {
-                        "tag": "a"
-                      },
-                      {
-                        "tag": "table",
-                        "children": [
-                          {
-                            "tag": "tbody",
-                            "children": [
-                              {
-                                "tag": "tr",
-                                "children": [
-                                  {
-                                    "tag": "td",
-                                    "children": [
-                                      {
-                                        "tag": "a",
-                                        "children": [
-                                          {
-                                            "tag": "table"
-                                          }
-                                        ]
-                                      },
-                                      {
-                                        "tag": "a"
-                                      }
-                                    ]
-                                  }
-                                ]
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "a"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><a><a></a><table><tbody><tr><td><a><table></table></a><a></a></td></tr></tbody></table></a><a></a></body></html>",
-        "noQuirksBodyHtml": "<a><a></a><table><tbody><tr><td><a><table></table></a><a></a></td></tr></tbody></table></a><a></a>"
-      }
-    },
-    {
-      "data": "<ul><li></li><div><li></div><li><li><div><li><address><li><b><em></b><li></ul>",
-      "errors": [
-        "(1,4): expected-doctype-but-got-start-tag",
-        "(1,45): end-tag-too-early",
-        "(1,58): end-tag-too-early",
-        "(1,69): adoption-agency-1.3"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "ul": true,
-            "li": true,
-            "div": true,
-            "address": true,
-            "b": true,
-            "em": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "ul",
-                    "children": [
-                      {
-                        "tag": "li"
-                      },
-                      {
-                        "tag": "div",
-                        "children": [
-                          {
-                            "tag": "li"
-                          }
-                        ]
-                      },
-                      {
-                        "tag": "li"
-                      },
-                      {
-                        "tag": "li",
-                        "children": [
-                          {
-                            "tag": "div"
-                          }
-                        ]
-                      },
-                      {
-                        "tag": "li",
-                        "children": [
-                          {
-                            "tag": "address"
-                          }
-                        ]
-                      },
-                      {
-                        "tag": "li",
-                        "children": [
-                          {
-                            "tag": "b",
-                            "children": [
-                              {
-                                "tag": "em"
-                              }
-                            ]
-                          }
-                        ]
-                      },
-                      {
-                        "tag": "li"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><ul><li></li><div><li></li></div><li></li><li><div></div></li><li><address></address></li><li><b><em></em></b></li><li></li></ul></body></html>",
-        "noQuirksBodyHtml": "<ul><li></li><div><li></li></div><li></li><li><div></div></li><li><address></address></li><li><b><em></em></b></li><li></li></ul>"
-      }
-    },
-    {
-      "data": "<ul><li><ul></li><li>a</li></ul></li></ul>",
-      "errors": [
-        "(1,4): expected-doctype-but-got-start-tag",
-        "(1,17): unexpected-end-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "ul": true,
-            "li": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "ul",
-                    "children": [
-                      {
-                        "tag": "li",
-                        "children": [
-                          {
-                            "tag": "ul",
-                            "children": [
-                              {
-                                "tag": "li",
-                                "children": [
-                                  {
-                                    "text": "a"
-                                  }
-                                ]
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><ul><li><ul><li>a</li></ul></li></ul></body></html>",
-        "noQuirksBodyHtml": "<ul><li><ul><li>a</li></ul></li></ul>"
-      }
-    },
-    {
-      "data": "<frameset><frame><frameset><frame></frameset><noframes></noframes></frameset>",
-      "errors": [
-        "(1,10): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "frameset": true,
-            "frame": true,
-            "noframes": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "frameset",
-                "children": [
-                  {
-                    "tag": "frame"
-                  },
-                  {
-                    "tag": "frameset",
-                    "children": [
-                      {
-                        "tag": "frame"
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "noframes"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><frameset><frame><frameset><frame></frameset><noframes></noframes></frameset></html>",
-        "noQuirksBodyHtml": "<noframes></noframes>"
-      }
-    },
-    {
-      "data": "<h1><table><td><h3></table><h3></h1>",
-      "errors": [
-        "(1,4): expected-doctype-but-got-start-tag",
-        "(1,15): unexpected-cell-in-table-body",
-        "(1,27): unexpected-cell-end-tag",
-        "(1,31): unexpected-start-tag",
-        "(1,36): end-tag-too-early"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "h1": true,
-            "table": true,
-            "tbody": true,
-            "tr": true,
-            "td": true,
-            "h3": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "h1",
-                    "children": [
-                      {
-                        "tag": "table",
-                        "children": [
-                          {
-                            "tag": "tbody",
-                            "children": [
-                              {
-                                "tag": "tr",
-                                "children": [
-                                  {
-                                    "tag": "td",
-                                    "children": [
-                                      {
-                                        "tag": "h3"
-                                      }
-                                    ]
-                                  }
-                                ]
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "h3"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><h1><table><tbody><tr><td><h3></h3></td></tr></tbody></table></h1><h3></h3></body></html>",
-        "noQuirksBodyHtml": "<h1><table><tbody><tr><td><h3></h3></td></tr></tbody></table></h1><h3></h3>"
-      }
-    },
-    {
-      "data": "<table><colgroup><col><colgroup><col><col><col><colgroup><col><col><thead><tr><td></table>",
-      "errors": [
-        "(1,7): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true,
-            "colgroup": true,
-            "col": true,
-            "thead": true,
-            "tr": true,
-            "td": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "colgroup",
-                        "children": [
-                          {
-                            "tag": "col"
-                          }
-                        ]
-                      },
-                      {
-                        "tag": "colgroup",
-                        "children": [
-                          {
-                            "tag": "col"
-                          },
-                          {
-                            "tag": "col"
-                          },
-                          {
-                            "tag": "col"
-                          }
-                        ]
-                      },
-                      {
-                        "tag": "colgroup",
-                        "children": [
-                          {
-                            "tag": "col"
-                          },
-                          {
-                            "tag": "col"
-                          }
-                        ]
-                      },
-                      {
-                        "tag": "thead",
-                        "children": [
-                          {
-                            "tag": "tr",
-                            "children": [
-                              {
-                                "tag": "td"
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><table><colgroup><col></colgroup><colgroup><col><col><col></colgroup><colgroup><col><col></colgroup><thead><tr><td></td></tr></thead></table></body></html>",
-        "noQuirksBodyHtml": "<table><colgroup><col></colgroup><colgroup><col><col><col></colgroup><colgroup><col><col></colgroup><thead><tr><td></td></tr></thead></table>"
-      }
-    },
-    {
-      "data": "<table><col><tbody><col><tr><col><td><col></table><col>",
-      "errors": [
-        "(1,7): expected-doctype-but-got-start-tag",
-        "(1,37): unexpected-cell-in-table-body",
-        "(1,55): unexpected-start-tag-ignored"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true,
-            "colgroup": true,
-            "col": true,
-            "tbody": true,
-            "tr": true,
-            "td": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "colgroup",
-                        "children": [
-                          {
-                            "tag": "col"
-                          }
-                        ]
-                      },
-                      {
-                        "tag": "tbody"
-                      },
-                      {
-                        "tag": "colgroup",
-                        "children": [
-                          {
-                            "tag": "col"
-                          }
-                        ]
-                      },
-                      {
-                        "tag": "tbody",
-                        "children": [
-                          {
-                            "tag": "tr"
-                          }
-                        ]
-                      },
-                      {
-                        "tag": "colgroup",
-                        "children": [
-                          {
-                            "tag": "col"
-                          }
-                        ]
-                      },
-                      {
-                        "tag": "tbody",
-                        "children": [
-                          {
-                            "tag": "tr",
-                            "children": [
-                              {
-                                "tag": "td"
-                              }
-                            ]
-                          }
-                        ]
-                      },
-                      {
-                        "tag": "colgroup",
-                        "children": [
-                          {
-                            "tag": "col"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><table><colgroup><col></colgroup><tbody></tbody><colgroup><col></colgroup><tbody><tr></tr></tbody><colgroup><col></colgroup><tbody><tr><td></td></tr></tbody><colgroup><col></colgroup></table></body></html>",
-        "noQuirksBodyHtml": "<table><colgroup><col></colgroup><tbody></tbody><colgroup><col></colgroup><tbody><tr></tr></tbody><colgroup><col></colgroup><tbody><tr><td></td></tr></tbody><colgroup><col></colgroup></table>"
-      }
-    },
-    {
-      "data": "<table><colgroup><tbody><colgroup><tr><colgroup><td><colgroup></table><colgroup>",
-      "errors": [
-        "(1,7): expected-doctype-but-got-start-tag",
-        "(1,52): unexpected-cell-in-table-body",
-        "(1,80): unexpected-start-tag-ignored"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true,
-            "colgroup": true,
-            "tbody": true,
-            "tr": true,
-            "td": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "colgroup"
-                      },
-                      {
-                        "tag": "tbody"
-                      },
-                      {
-                        "tag": "colgroup"
-                      },
-                      {
-                        "tag": "tbody",
-                        "children": [
-                          {
-                            "tag": "tr"
-                          }
-                        ]
-                      },
-                      {
-                        "tag": "colgroup"
-                      },
-                      {
-                        "tag": "tbody",
-                        "children": [
-                          {
-                            "tag": "tr",
-                            "children": [
-                              {
-                                "tag": "td"
-                              }
-                            ]
-                          }
-                        ]
-                      },
-                      {
-                        "tag": "colgroup"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><table><colgroup></colgroup><tbody></tbody><colgroup></colgroup><tbody><tr></tr></tbody><colgroup></colgroup><tbody><tr><td></td></tr></tbody><colgroup></colgroup></table></body></html>",
-        "noQuirksBodyHtml": "<table><colgroup></colgroup><tbody></tbody><colgroup></colgroup><tbody><tr></tr></tbody><colgroup></colgroup><tbody><tr><td></td></tr></tbody><colgroup></colgroup></table>"
-      }
-    },
-    {
-      "data": "</strong></b></em></i></u></strike></s></blink></tt></pre></big></small></font></select></h1></h2></h3></h4></h5></h6></body></br></a></img></title></span></style></script></table></th></td></tr></frame></area></link></param></hr></input></col></base></meta></basefont></bgsound></embed></spacer></p></dd></dt></caption></colgroup></tbody></tfoot></thead></address></blockquote></center></dir></div></dl></fieldset></listing></menu></ol></ul></li></nobr></wbr></form></button></marquee></object></html></frameset></head></iframe></image></isindex></noembed></noframes></noscript></optgroup></option></plaintext></textarea>",
-      "errors": [
-        "(1,9): expected-doctype-but-got-end-tag",
-        "(1,9): unexpected-end-tag-before-html",
-        "(1,13): unexpected-end-tag-before-html",
-        "(1,18): unexpected-end-tag-before-html",
-        "(1,22): unexpected-end-tag-before-html",
-        "(1,26): unexpected-end-tag-before-html",
-        "(1,35): unexpected-end-tag-before-html",
-        "(1,39): unexpected-end-tag-before-html",
-        "(1,47): unexpected-end-tag-before-html",
-        "(1,52): unexpected-end-tag-before-html",
-        "(1,58): unexpected-end-tag-before-html",
-        "(1,64): unexpected-end-tag-before-html",
-        "(1,72): unexpected-end-tag-before-html",
-        "(1,79): unexpected-end-tag-before-html",
-        "(1,88): unexpected-end-tag-before-html",
-        "(1,93): unexpected-end-tag-before-html",
-        "(1,98): unexpected-end-tag-before-html",
-        "(1,103): unexpected-end-tag-before-html",
-        "(1,108): unexpected-end-tag-before-html",
-        "(1,113): unexpected-end-tag-before-html",
-        "(1,118): unexpected-end-tag-before-html",
-        "(1,130): unexpected-end-tag-after-body",
-        "(1,130): unexpected-end-tag-treated-as",
-        "(1,134): unexpected-end-tag",
-        "(1,140): unexpected-end-tag",
-        "(1,148): unexpected-end-tag",
-        "(1,155): unexpected-end-tag",
-        "(1,163): unexpected-end-tag",
-        "(1,172): unexpected-end-tag",
-        "(1,180): unexpected-end-tag",
-        "(1,185): unexpected-end-tag",
-        "(1,190): unexpected-end-tag",
-        "(1,195): unexpected-end-tag",
-        "(1,203): unexpected-end-tag",
-        "(1,210): unexpected-end-tag",
-        "(1,217): unexpected-end-tag",
-        "(1,225): unexpected-end-tag",
-        "(1,230): unexpected-end-tag",
-        "(1,238): unexpected-end-tag",
-        "(1,244): unexpected-end-tag",
-        "(1,251): unexpected-end-tag",
-        "(1,258): unexpected-end-tag",
-        "(1,269): unexpected-end-tag",
-        "(1,279): unexpected-end-tag",
-        "(1,287): unexpected-end-tag",
-        "(1,296): unexpected-end-tag",
-        "(1,300): unexpected-end-tag",
-        "(1,305): unexpected-end-tag",
-        "(1,310): unexpected-end-tag",
-        "(1,320): unexpected-end-tag",
-        "(1,331): unexpected-end-tag",
-        "(1,339): unexpected-end-tag",
-        "(1,347): unexpected-end-tag",
-        "(1,355): unexpected-end-tag",
-        "(1,365): end-tag-too-early",
-        "(1,378): end-tag-too-early",
-        "(1,387): end-tag-too-early",
-        "(1,393): end-tag-too-early",
-        "(1,399): end-tag-too-early",
-        "(1,404): end-tag-too-early",
-        "(1,415): end-tag-too-early",
-        "(1,425): end-tag-too-early",
-        "(1,432): end-tag-too-early",
-        "(1,437): end-tag-too-early",
-        "(1,442): end-tag-too-early",
-        "(1,447): unexpected-end-tag",
-        "(1,454): unexpected-end-tag",
-        "(1,460): unexpected-end-tag",
-        "(1,467): unexpected-end-tag",
-        "(1,476): end-tag-too-early",
-        "(1,486): end-tag-too-early",
-        "(1,495): end-tag-too-early",
-        "(1,513): expected-eof-but-got-end-tag",
-        "(1,513): unexpected-end-tag",
-        "(1,520): unexpected-end-tag",
-        "(1,529): unexpected-end-tag",
-        "(1,537): unexpected-end-tag",
-        "(1,547): unexpected-end-tag",
-        "(1,557): unexpected-end-tag",
-        "(1,568): unexpected-end-tag",
-        "(1,579): unexpected-end-tag",
-        "(1,590): unexpected-end-tag",
-        "(1,599): unexpected-end-tag",
-        "(1,611): unexpected-end-tag",
-        "(1,622): unexpected-end-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "br": true,
-            "p": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "br"
-                  },
-                  {
-                    "tag": "p"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><br><p></p></body></html>",
-        "noQuirksBodyHtml": "<br><p></p>"
-      }
-    },
-    {
-      "data": "<table><tr></strong></b></em></i></u></strike></s></blink></tt></pre></big></small></font></select></h1></h2></h3></h4></h5></h6></body></br></a></img></title></span></style></script></table></th></td></tr></frame></area></link></param></hr></input></col></base></meta></basefont></bgsound></embed></spacer></p></dd></dt></caption></colgroup></tbody></tfoot></thead></address></blockquote></center></dir></div></dl></fieldset></listing></menu></ol></ul></li></nobr></wbr></form></button></marquee></object></html></frameset></head></iframe></image></isindex></noembed></noframes></noscript></optgroup></option></plaintext></textarea>",
-      "errors": [
-        "(1,7): expected-doctype-but-got-start-tag",
-        "(1,20): unexpected-end-tag-implies-table-voodoo",
-        "(1,20): unexpected-end-tag",
-        "(1,24): unexpected-end-tag-implies-table-voodoo",
-        "(1,24): unexpected-end-tag",
-        "(1,29): unexpected-end-tag-implies-table-voodoo",
-        "(1,29): unexpected-end-tag",
-        "(1,33): unexpected-end-tag-implies-table-voodoo",
-        "(1,33): unexpected-end-tag",
-        "(1,37): unexpected-end-tag-implies-table-voodoo",
-        "(1,37): unexpected-end-tag",
-        "(1,46): unexpected-end-tag-implies-table-voodoo",
-        "(1,46): unexpected-end-tag",
-        "(1,50): unexpected-end-tag-implies-table-voodoo",
-        "(1,50): unexpected-end-tag",
-        "(1,58): unexpected-end-tag-implies-table-voodoo",
-        "(1,58): unexpected-end-tag",
-        "(1,63): unexpected-end-tag-implies-table-voodoo",
-        "(1,63): unexpected-end-tag",
-        "(1,69): unexpected-end-tag-implies-table-voodoo",
-        "(1,69): end-tag-too-early",
-        "(1,75): unexpected-end-tag-implies-table-voodoo",
-        "(1,75): unexpected-end-tag",
-        "(1,83): unexpected-end-tag-implies-table-voodoo",
-        "(1,83): unexpected-end-tag",
-        "(1,90): unexpected-end-tag-implies-table-voodoo",
-        "(1,90): unexpected-end-tag",
-        "(1,99): unexpected-end-tag-implies-table-voodoo",
-        "(1,99): unexpected-end-tag",
-        "(1,104): unexpected-end-tag-implies-table-voodoo",
-        "(1,104): end-tag-too-early",
-        "(1,109): unexpected-end-tag-implies-table-voodoo",
-        "(1,109): end-tag-too-early",
-        "(1,114): unexpected-end-tag-implies-table-voodoo",
-        "(1,114): end-tag-too-early",
-        "(1,119): unexpected-end-tag-implies-table-voodoo",
-        "(1,119): end-tag-too-early",
-        "(1,124): unexpected-end-tag-implies-table-voodoo",
-        "(1,124): end-tag-too-early",
-        "(1,129): unexpected-end-tag-implies-table-voodoo",
-        "(1,129): end-tag-too-early",
-        "(1,136): unexpected-end-tag-in-table-row",
-        "(1,141): unexpected-end-tag-implies-table-voodoo",
-        "(1,141): unexpected-end-tag-treated-as",
-        "(1,145): unexpected-end-tag-implies-table-voodoo",
-        "(1,145): unexpected-end-tag",
-        "(1,151): unexpected-end-tag-implies-table-voodoo",
-        "(1,151): unexpected-end-tag",
-        "(1,159): unexpected-end-tag-implies-table-voodoo",
-        "(1,159): unexpected-end-tag",
-        "(1,166): unexpected-end-tag-implies-table-voodoo",
-        "(1,166): unexpected-end-tag",
-        "(1,174): unexpected-end-tag-implies-table-voodoo",
-        "(1,174): unexpected-end-tag",
-        "(1,183): unexpected-end-tag-implies-table-voodoo",
-        "(1,183): unexpected-end-tag",
-        "(1,196): unexpected-end-tag",
-        "(1,201): unexpected-end-tag",
-        "(1,206): unexpected-end-tag",
-        "(1,214): unexpected-end-tag",
-        "(1,221): unexpected-end-tag",
-        "(1,228): unexpected-end-tag",
-        "(1,236): unexpected-end-tag",
-        "(1,241): unexpected-end-tag",
-        "(1,249): unexpected-end-tag",
-        "(1,255): unexpected-end-tag",
-        "(1,262): unexpected-end-tag",
-        "(1,269): unexpected-end-tag",
-        "(1,280): unexpected-end-tag",
-        "(1,290): unexpected-end-tag",
-        "(1,298): unexpected-end-tag",
-        "(1,307): unexpected-end-tag",
-        "(1,311): unexpected-end-tag",
-        "(1,316): unexpected-end-tag",
-        "(1,321): unexpected-end-tag",
-        "(1,331): unexpected-end-tag",
-        "(1,342): unexpected-end-tag",
-        "(1,350): unexpected-end-tag",
-        "(1,358): unexpected-end-tag",
-        "(1,366): unexpected-end-tag",
-        "(1,376): end-tag-too-early",
-        "(1,389): end-tag-too-early",
-        "(1,398): end-tag-too-early",
-        "(1,404): end-tag-too-early",
-        "(1,410): end-tag-too-early",
-        "(1,415): end-tag-too-early",
-        "(1,426): end-tag-too-early",
-        "(1,436): end-tag-too-early",
-        "(1,443): end-tag-too-early",
-        "(1,448): end-tag-too-early",
-        "(1,453): end-tag-too-early",
-        "(1,458): unexpected-end-tag",
-        "(1,465): unexpected-end-tag",
-        "(1,471): unexpected-end-tag",
-        "(1,478): unexpected-end-tag",
-        "(1,487): end-tag-too-early",
-        "(1,497): end-tag-too-early",
-        "(1,506): end-tag-too-early",
-        "(1,524): expected-eof-but-got-end-tag",
-        "(1,524): unexpected-end-tag",
-        "(1,531): unexpected-end-tag",
-        "(1,540): unexpected-end-tag",
-        "(1,548): unexpected-end-tag",
-        "(1,558): unexpected-end-tag",
-        "(1,568): unexpected-end-tag",
-        "(1,579): unexpected-end-tag",
-        "(1,590): unexpected-end-tag",
-        "(1,601): unexpected-end-tag",
-        "(1,610): unexpected-end-tag",
-        "(1,622): unexpected-end-tag",
-        "(1,633): unexpected-end-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "br": true,
-            "table": true,
-            "tbody": true,
-            "tr": true,
-            "p": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "br"
-                  },
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "tbody",
-                        "children": [
-                          {
-                            "tag": "tr"
-                          }
-                        ]
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "p"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><br><table><tbody><tr></tr></tbody></table><p></p></body></html>",
-        "noQuirksBodyHtml": "<br><table><tbody><tr></tr></tbody></table><p></p>"
-      }
-    },
-    {
-      "data": "<frameset>",
-      "errors": [
-        "(1,10): expected-doctype-but-got-start-tag",
-        "(1,10): eof-in-frameset"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "frameset": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "frameset"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><frameset></frameset></html>",
-        "noQuirksBodyHtml": ""
-      }
-    }
-  ],
-  "tests10.dat": [
-    {
-      "data": "<!DOCTYPE html><svg></svg>",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "svg svg": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "svg",
-                    "ns": "http://www.w3.org/2000/svg"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><svg></svg></body></html>",
-        "noQuirksBodyHtml": "<svg></svg>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><svg></svg><![CDATA[a]]>",
-      "errors": [
-        "(1,28) expected-dashes-or-doctype"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "svg svg": true
-          },
-          "doctype": true,
-          "comment": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "svg",
-                    "ns": "http://www.w3.org/2000/svg"
-                  },
-                  {
-                    "comment": "[CDATA[a]]"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><svg></svg><!--[CDATA[a]]--></body></html>",
-        "noQuirksBodyHtml": "<svg></svg><!--[CDATA[a]]-->"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><body><svg></svg>",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "svg svg": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "svg",
-                    "ns": "http://www.w3.org/2000/svg"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><svg></svg></body></html>",
-        "noQuirksBodyHtml": "<svg></svg>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><body><select><svg></svg></select>",
-      "errors": [
-        "(1,34) unexpected-start-tag-in-select",
-        "(1,40) unexpected-end-tag-in-select"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "select": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "select"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><select></select></body></html>",
-        "noQuirksBodyHtml": "<select></select>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><body><select><option><svg></svg></option></select>",
-      "errors": [
-        "(1,42) unexpected-start-tag-in-select",
-        "(1,48) unexpected-end-tag-in-select"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "select": true,
-            "option": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "select",
-                    "children": [
-                      {
-                        "tag": "option"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><select><option></option></select></body></html>",
-        "noQuirksBodyHtml": "<select><option></option></select>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><body><table><svg></svg></table>",
-      "errors": [
-        "(1,33) foster-parenting-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "svg svg": true,
-            "table": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "svg",
-                    "ns": "http://www.w3.org/2000/svg"
-                  },
-                  {
-                    "tag": "table"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><svg></svg><table></table></body></html>",
-        "noQuirksBodyHtml": "<svg></svg><table></table>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><body><table><svg><g>foo</g></svg></table>",
-      "errors": [
-        "(1,33) foster-parenting-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "svg svg": true,
-            "svg g": true,
-            "table": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "svg",
-                    "ns": "http://www.w3.org/2000/svg",
-                    "children": [
-                      {
-                        "tag": "g",
-                        "ns": "http://www.w3.org/2000/svg",
-                        "children": [
-                          {
-                            "text": "foo"
-                          }
-                        ]
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "table"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><svg><g>foo</g></svg><table></table></body></html>",
-        "noQuirksBodyHtml": "<svg><g>foo</g></svg><table></table>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><body><table><svg><g>foo</g><g>bar</g></svg></table>",
-      "errors": [
-        "(1,33) foster-parenting-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "svg svg": true,
-            "svg g": true,
-            "table": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "svg",
-                    "ns": "http://www.w3.org/2000/svg",
-                    "children": [
-                      {
-                        "tag": "g",
-                        "ns": "http://www.w3.org/2000/svg",
-                        "children": [
-                          {
-                            "text": "foo"
-                          }
-                        ]
-                      },
-                      {
-                        "tag": "g",
-                        "ns": "http://www.w3.org/2000/svg",
-                        "children": [
-                          {
-                            "text": "bar"
-                          }
-                        ]
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "table"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><svg><g>foo</g><g>bar</g></svg><table></table></body></html>",
-        "noQuirksBodyHtml": "<svg><g>foo</g><g>bar</g></svg><table></table>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><body><table><tbody><svg><g>foo</g><g>bar</g></svg></tbody></table>",
-      "errors": [
-        "(1,40) foster-parenting-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "svg svg": true,
-            "svg g": true,
-            "table": true,
-            "tbody": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "svg",
-                    "ns": "http://www.w3.org/2000/svg",
-                    "children": [
-                      {
-                        "tag": "g",
-                        "ns": "http://www.w3.org/2000/svg",
-                        "children": [
-                          {
-                            "text": "foo"
-                          }
-                        ]
-                      },
-                      {
-                        "tag": "g",
-                        "ns": "http://www.w3.org/2000/svg",
-                        "children": [
-                          {
-                            "text": "bar"
-                          }
-                        ]
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "tbody"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><svg><g>foo</g><g>bar</g></svg><table><tbody></tbody></table></body></html>",
-        "noQuirksBodyHtml": "<svg><g>foo</g><g>bar</g></svg><table><tbody></tbody></table>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><body><table><tbody><tr><svg><g>foo</g><g>bar</g></svg></tr></tbody></table>",
-      "errors": [
-        "(1,44) foster-parenting-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "svg svg": true,
-            "svg g": true,
-            "table": true,
-            "tbody": true,
-            "tr": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "svg",
-                    "ns": "http://www.w3.org/2000/svg",
-                    "children": [
-                      {
-                        "tag": "g",
-                        "ns": "http://www.w3.org/2000/svg",
-                        "children": [
-                          {
-                            "text": "foo"
-                          }
-                        ]
-                      },
-                      {
-                        "tag": "g",
-                        "ns": "http://www.w3.org/2000/svg",
-                        "children": [
-                          {
-                            "text": "bar"
-                          }
-                        ]
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "tbody",
-                        "children": [
-                          {
-                            "tag": "tr"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><svg><g>foo</g><g>bar</g></svg><table><tbody><tr></tr></tbody></table></body></html>",
-        "noQuirksBodyHtml": "<svg><g>foo</g><g>bar</g></svg><table><tbody><tr></tr></tbody></table>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><body><table><tbody><tr><td><svg><g>foo</g><g>bar</g></svg></td></tr></tbody></table>",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true,
-            "tbody": true,
-            "tr": true,
-            "td": true,
-            "svg svg": true,
-            "svg g": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "tbody",
-                        "children": [
-                          {
-                            "tag": "tr",
-                            "children": [
-                              {
-                                "tag": "td",
-                                "children": [
-                                  {
-                                    "tag": "svg",
-                                    "ns": "http://www.w3.org/2000/svg",
-                                    "children": [
-                                      {
-                                        "tag": "g",
-                                        "ns": "http://www.w3.org/2000/svg",
-                                        "children": [
-                                          {
-                                            "text": "foo"
-                                          }
-                                        ]
-                                      },
-                                      {
-                                        "tag": "g",
-                                        "ns": "http://www.w3.org/2000/svg",
-                                        "children": [
-                                          {
-                                            "text": "bar"
-                                          }
-                                        ]
-                                      }
-                                    ]
-                                  }
-                                ]
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><table><tbody><tr><td><svg><g>foo</g><g>bar</g></svg></td></tr></tbody></table></body></html>",
-        "noQuirksBodyHtml": "<table><tbody><tr><td><svg><g>foo</g><g>bar</g></svg></td></tr></tbody></table>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><body><table><tbody><tr><td><svg><g>foo</g><g>bar</g></svg><p>baz</td></tr></tbody></table>",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true,
-            "tbody": true,
-            "tr": true,
-            "td": true,
-            "svg svg": true,
-            "svg g": true,
-            "p": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "tbody",
-                        "children": [
-                          {
-                            "tag": "tr",
-                            "children": [
-                              {
-                                "tag": "td",
-                                "children": [
-                                  {
-                                    "tag": "svg",
-                                    "ns": "http://www.w3.org/2000/svg",
-                                    "children": [
-                                      {
-                                        "tag": "g",
-                                        "ns": "http://www.w3.org/2000/svg",
-                                        "children": [
-                                          {
-                                            "text": "foo"
-                                          }
-                                        ]
-                                      },
-                                      {
-                                        "tag": "g",
-                                        "ns": "http://www.w3.org/2000/svg",
-                                        "children": [
-                                          {
-                                            "text": "bar"
-                                          }
-                                        ]
-                                      }
-                                    ]
-                                  },
-                                  {
-                                    "tag": "p",
-                                    "children": [
-                                      {
-                                        "text": "baz"
-                                      }
-                                    ]
-                                  }
-                                ]
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><table><tbody><tr><td><svg><g>foo</g><g>bar</g></svg><p>baz</p></td></tr></tbody></table></body></html>",
-        "noQuirksBodyHtml": "<table><tbody><tr><td><svg><g>foo</g><g>bar</g></svg><p>baz</p></td></tr></tbody></table>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><body><table><caption><svg><g>foo</g><g>bar</g></svg><p>baz</caption></table>",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true,
-            "caption": true,
-            "svg svg": true,
-            "svg g": true,
-            "p": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "caption",
-                        "children": [
-                          {
-                            "tag": "svg",
-                            "ns": "http://www.w3.org/2000/svg",
-                            "children": [
-                              {
-                                "tag": "g",
-                                "ns": "http://www.w3.org/2000/svg",
-                                "children": [
-                                  {
-                                    "text": "foo"
-                                  }
-                                ]
-                              },
-                              {
-                                "tag": "g",
-                                "ns": "http://www.w3.org/2000/svg",
-                                "children": [
-                                  {
-                                    "text": "bar"
-                                  }
-                                ]
-                              }
-                            ]
-                          },
-                          {
-                            "tag": "p",
-                            "children": [
-                              {
-                                "text": "baz"
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><table><caption><svg><g>foo</g><g>bar</g></svg><p>baz</p></caption></table></body></html>",
-        "noQuirksBodyHtml": "<table><caption><svg><g>foo</g><g>bar</g></svg><p>baz</p></caption></table>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><body><table><caption><svg><g>foo</g><g>bar</g><p>baz</table><p>quux",
-      "errors": [
-        "(1,65) unexpected-html-element-in-foreign-content"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true,
-            "caption": true,
-            "svg svg": true,
-            "svg g": true,
-            "p": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "caption",
-                        "children": [
-                          {
-                            "tag": "svg",
-                            "ns": "http://www.w3.org/2000/svg",
-                            "children": [
-                              {
-                                "tag": "g",
-                                "ns": "http://www.w3.org/2000/svg",
-                                "children": [
-                                  {
-                                    "text": "foo"
-                                  }
-                                ]
-                              },
-                              {
-                                "tag": "g",
-                                "ns": "http://www.w3.org/2000/svg",
-                                "children": [
-                                  {
-                                    "text": "bar"
-                                  }
-                                ]
-                              }
-                            ]
-                          },
-                          {
-                            "tag": "p",
-                            "children": [
-                              {
-                                "text": "baz"
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "p",
-                    "children": [
-                      {
-                        "text": "quux"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><table><caption><svg><g>foo</g><g>bar</g></svg><p>baz</p></caption></table><p>quux</p></body></html>",
-        "noQuirksBodyHtml": "<table><caption><svg><g>foo</g><g>bar</g><p>baz</p></svg></caption></table><p>quux</p>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><body><table><caption><svg><g>foo</g><g>bar</g>baz</table><p>quux",
-      "errors": [
-        "(1,73) unexpected-end-tag",
-        "(1,73) expected-one-end-tag-but-got-another"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true,
-            "caption": true,
-            "svg svg": true,
-            "svg g": true,
-            "p": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "caption",
-                        "children": [
-                          {
-                            "tag": "svg",
-                            "ns": "http://www.w3.org/2000/svg",
-                            "children": [
-                              {
-                                "tag": "g",
-                                "ns": "http://www.w3.org/2000/svg",
-                                "children": [
-                                  {
-                                    "text": "foo"
-                                  }
-                                ]
-                              },
-                              {
-                                "tag": "g",
-                                "ns": "http://www.w3.org/2000/svg",
-                                "children": [
-                                  {
-                                    "text": "bar"
-                                  }
-                                ]
-                              },
-                              {
-                                "text": "baz"
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "p",
-                    "children": [
-                      {
-                        "text": "quux"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><table><caption><svg><g>foo</g><g>bar</g>baz</svg></caption></table><p>quux</p></body></html>",
-        "noQuirksBodyHtml": "<table><caption><svg><g>foo</g><g>bar</g>baz</svg></caption></table><p>quux</p>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><body><table><colgroup><svg><g>foo</g><g>bar</g><p>baz</table><p>quux",
-      "errors": [
-        "(1,43) foster-parenting-start-tag svg",
-        "(1,66) unexpected HTML-like start tag token in foreign content",
-        "(1,66) foster-parenting-start-tag",
-        "(1,67) foster-parenting-character",
-        "(1,68) foster-parenting-character",
-        "(1,69) foster-parenting-character"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "svg svg": true,
-            "svg g": true,
-            "p": true,
-            "table": true,
-            "colgroup": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "svg",
-                    "ns": "http://www.w3.org/2000/svg",
-                    "children": [
-                      {
-                        "tag": "g",
-                        "ns": "http://www.w3.org/2000/svg",
-                        "children": [
-                          {
-                            "text": "foo"
-                          }
-                        ]
-                      },
-                      {
-                        "tag": "g",
-                        "ns": "http://www.w3.org/2000/svg",
-                        "children": [
-                          {
-                            "text": "bar"
-                          }
-                        ]
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "p",
-                    "children": [
-                      {
-                        "text": "baz"
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "colgroup"
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "p",
-                    "children": [
-                      {
-                        "text": "quux"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><svg><g>foo</g><g>bar</g></svg><p>baz</p><table><colgroup></colgroup></table><p>quux</p></body></html>",
-        "noQuirksBodyHtml": "<svg><g>foo</g><g>bar</g><p>baz</p></svg><table><colgroup></colgroup></table><p>quux</p>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><body><table><tr><td><select><svg><g>foo</g><g>bar</g><p>baz</table><p>quux",
-      "errors": [
-        "(1,49) unexpected-start-tag-in-select",
-        "(1,52) unexpected-start-tag-in-select",
-        "(1,59) unexpected-end-tag-in-select",
-        "(1,62) unexpected-start-tag-in-select",
-        "(1,69) unexpected-end-tag-in-select",
-        "(1,72) unexpected-start-tag-in-select",
-        "(1,83) unexpected-table-element-end-tag-in-select-in-table"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true,
-            "tbody": true,
-            "tr": true,
-            "td": true,
-            "select": true,
-            "p": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "tbody",
-                        "children": [
-                          {
-                            "tag": "tr",
-                            "children": [
-                              {
-                                "tag": "td",
-                                "children": [
-                                  {
-                                    "tag": "select",
-                                    "children": [
-                                      {
-                                        "text": "foobarbaz"
-                                      }
-                                    ]
-                                  }
-                                ]
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "p",
-                    "children": [
-                      {
-                        "text": "quux"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><table><tbody><tr><td><select>foobarbaz</select></td></tr></tbody></table><p>quux</p></body></html>",
-        "noQuirksBodyHtml": "<table><tbody><tr><td><select>foobarbaz</select></td></tr></tbody></table><p>quux</p>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><body><table><select><svg><g>foo</g><g>bar</g><p>baz</table><p>quux",
-      "errors": [
-        "(1,36) unexpected-start-tag-implies-table-voodoo",
-        "(1,41) unexpected-start-tag-in-select",
-        "(1,44) unexpected-start-tag-in-select",
-        "(1,51) unexpected-end-tag-in-select",
-        "(1,54) unexpected-start-tag-in-select",
-        "(1,61) unexpected-end-tag-in-select",
-        "(1,64) unexpected-start-tag-in-select",
-        "(1,75) unexpected-table-element-end-tag-in-select-in-table"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "select": true,
-            "table": true,
-            "p": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "select",
-                    "children": [
-                      {
-                        "text": "foobarbaz"
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "table"
-                  },
-                  {
-                    "tag": "p",
-                    "children": [
-                      {
-                        "text": "quux"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><select>foobarbaz</select><table></table><p>quux</p></body></html>",
-        "noQuirksBodyHtml": "<select>foobarbaz</select><table></table><p>quux</p>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><body></body></html><svg><g>foo</g><g>bar</g><p>baz",
-      "errors": [
-        "(1,40) expected-eof-but-got-start-tag",
-        "(1,63) unexpected-html-element-in-foreign-content"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "svg svg": true,
-            "svg g": true,
-            "p": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "svg",
-                    "ns": "http://www.w3.org/2000/svg",
-                    "children": [
-                      {
-                        "tag": "g",
-                        "ns": "http://www.w3.org/2000/svg",
-                        "children": [
-                          {
-                            "text": "foo"
-                          }
-                        ]
-                      },
-                      {
-                        "tag": "g",
-                        "ns": "http://www.w3.org/2000/svg",
-                        "children": [
-                          {
-                            "text": "bar"
-                          }
-                        ]
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "p",
-                    "children": [
-                      {
-                        "text": "baz"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><svg><g>foo</g><g>bar</g></svg><p>baz</p></body></html>",
-        "noQuirksBodyHtml": "<svg><g>foo</g><g>bar</g><p>baz</p></svg>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><body></body><svg><g>foo</g><g>bar</g><p>baz",
-      "errors": [
-        "(1,33) unexpected-start-tag-after-body",
-        "(1,56) unexpected-html-element-in-foreign-content"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "svg svg": true,
-            "svg g": true,
-            "p": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "svg",
-                    "ns": "http://www.w3.org/2000/svg",
-                    "children": [
-                      {
-                        "tag": "g",
-                        "ns": "http://www.w3.org/2000/svg",
-                        "children": [
-                          {
-                            "text": "foo"
-                          }
-                        ]
-                      },
-                      {
-                        "tag": "g",
-                        "ns": "http://www.w3.org/2000/svg",
-                        "children": [
-                          {
-                            "text": "bar"
-                          }
-                        ]
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "p",
-                    "children": [
-                      {
-                        "text": "baz"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><svg><g>foo</g><g>bar</g></svg><p>baz</p></body></html>",
-        "noQuirksBodyHtml": "<svg><g>foo</g><g>bar</g><p>baz</p></svg>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><frameset><svg><g></g><g></g><p><span>",
-      "errors": [
-        "(1,30) unexpected-start-tag-in-frameset",
-        "(1,33) unexpected-start-tag-in-frameset",
-        "(1,37) unexpected-end-tag-in-frameset",
-        "(1,40) unexpected-start-tag-in-frameset",
-        "(1,44) unexpected-end-tag-in-frameset",
-        "(1,47) unexpected-start-tag-in-frameset",
-        "(1,53) unexpected-start-tag-in-frameset",
-        "(1,53) eof-in-frameset"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "frameset": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "frameset"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><frameset></frameset></html>",
-        "noQuirksBodyHtml": "<svg><g></g><g></g><p><span></span></p></svg>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><frameset></frameset><svg><g></g><g></g><p><span>",
-      "errors": [
-        "(1,41) unexpected-start-tag-after-frameset",
-        "(1,44) unexpected-start-tag-after-frameset",
-        "(1,48) unexpected-end-tag-after-frameset",
-        "(1,51) unexpected-start-tag-after-frameset",
-        "(1,55) unexpected-end-tag-after-frameset",
-        "(1,58) unexpected-start-tag-after-frameset",
-        "(1,64) unexpected-start-tag-after-frameset"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "frameset": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "frameset"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><frameset></frameset></html>",
-        "noQuirksBodyHtml": "<svg><g></g><g></g><p><span></span></p></svg>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><body xlink:href=foo><svg xlink:href=foo></svg>",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "svg svg": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "attrs": [
-                  {
-                    "name": "xlink:href",
-                    "value": "foo"
-                  }
-                ],
-                "children": [
-                  {
-                    "tag": "svg",
-                    "ns": "http://www.w3.org/2000/svg",
-                    "attrs": [
-                      {
-                        "name": "href",
-                        "ns": "http://www.w3.org/1999/xlink",
-                        "value": "foo"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body xlink:href=\"foo\"><svg xlink:href=\"foo\"></svg></body></html>",
-        "noQuirksBodyHtml": "<svg xlink:href=\"foo\"></svg>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><body xlink:href=foo xml:lang=en><svg><g xml:lang=en xlink:href=foo></g></svg>",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "svg svg": true,
-            "svg g": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "attrs": [
-                  {
-                    "name": "xlink:href",
-                    "value": "foo"
-                  },
-                  {
-                    "name": "xml:lang",
-                    "value": "en"
-                  }
-                ],
-                "children": [
-                  {
-                    "tag": "svg",
-                    "ns": "http://www.w3.org/2000/svg",
-                    "children": [
-                      {
-                        "tag": "g",
-                        "ns": "http://www.w3.org/2000/svg",
-                        "attrs": [
-                          {
-                            "name": "href",
-                            "ns": "http://www.w3.org/1999/xlink",
-                            "value": "foo"
-                          },
-                          {
-                            "name": "lang",
-                            "ns": "http://www.w3.org/XML/1998/namespace",
-                            "value": "en"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body xlink:href=\"foo\" xml:lang=\"en\"><svg><g xml:lang=\"en\" xlink:href=\"foo\"></g></svg></body></html>",
-        "noQuirksBodyHtml": "<svg><g xml:lang=\"en\" xlink:href=\"foo\"></g></svg>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><body xlink:href=foo xml:lang=en><svg><g xml:lang=en xlink:href=foo /></svg>",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "svg svg": true,
-            "svg g": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "attrs": [
-                  {
-                    "name": "xlink:href",
-                    "value": "foo"
-                  },
-                  {
-                    "name": "xml:lang",
-                    "value": "en"
-                  }
-                ],
-                "children": [
-                  {
-                    "tag": "svg",
-                    "ns": "http://www.w3.org/2000/svg",
-                    "children": [
-                      {
-                        "tag": "g",
-                        "ns": "http://www.w3.org/2000/svg",
-                        "attrs": [
-                          {
-                            "name": "href",
-                            "ns": "http://www.w3.org/1999/xlink",
-                            "value": "foo"
-                          },
-                          {
-                            "name": "lang",
-                            "ns": "http://www.w3.org/XML/1998/namespace",
-                            "value": "en"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body xlink:href=\"foo\" xml:lang=\"en\"><svg><g xml:lang=\"en\" xlink:href=\"foo\"></g></svg></body></html>",
-        "noQuirksBodyHtml": "<svg><g xml:lang=\"en\" xlink:href=\"foo\"></g></svg>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><body xlink:href=foo xml:lang=en><svg><g xml:lang=en xlink:href=foo />bar</svg>",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "svg svg": true,
-            "svg g": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "attrs": [
-                  {
-                    "name": "xlink:href",
-                    "value": "foo"
-                  },
-                  {
-                    "name": "xml:lang",
-                    "value": "en"
-                  }
-                ],
-                "children": [
-                  {
-                    "tag": "svg",
-                    "ns": "http://www.w3.org/2000/svg",
-                    "children": [
-                      {
-                        "tag": "g",
-                        "ns": "http://www.w3.org/2000/svg",
-                        "attrs": [
-                          {
-                            "name": "href",
-                            "ns": "http://www.w3.org/1999/xlink",
-                            "value": "foo"
-                          },
-                          {
-                            "name": "lang",
-                            "ns": "http://www.w3.org/XML/1998/namespace",
-                            "value": "en"
-                          }
-                        ]
-                      },
-                      {
-                        "text": "bar"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body xlink:href=\"foo\" xml:lang=\"en\"><svg><g xml:lang=\"en\" xlink:href=\"foo\"></g>bar</svg></body></html>",
-        "noQuirksBodyHtml": "<svg><g xml:lang=\"en\" xlink:href=\"foo\"></g>bar</svg>"
-      }
-    },
-    {
-      "data": "<svg></path>",
-      "errors": [
-        "(1,5) expected-doctype-but-got-start-tag",
-        "(1,12) unexpected-end-tag",
-        "(1,12) unexpected-end-tag",
-        "(1,12) expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "svg svg": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "svg",
-                    "ns": "http://www.w3.org/2000/svg"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><svg></svg></body></html>",
-        "noQuirksBodyHtml": "<svg></svg>"
-      }
-    },
-    {
-      "data": "<div><svg></div>a",
-      "errors": [
-        "(1,5) expected-doctype-but-got-start-tag",
-        "(1,16) unexpected-end-tag",
-        "(1,16) end-tag-too-early"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "div": true,
-            "svg svg": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "div",
-                    "children": [
-                      {
-                        "tag": "svg",
-                        "ns": "http://www.w3.org/2000/svg"
-                      }
-                    ]
-                  },
-                  {
-                    "text": "a"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><div><svg></svg></div>a</body></html>",
-        "noQuirksBodyHtml": "<div><svg></svg></div>a"
-      }
-    },
-    {
-      "data": "<div><svg><path></div>a",
-      "errors": [
-        "(1,5) expected-doctype-but-got-start-tag",
-        "(1,22) unexpected-end-tag",
-        "(1,22) end-tag-too-early"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "div": true,
-            "svg svg": true,
-            "svg path": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "div",
-                    "children": [
-                      {
-                        "tag": "svg",
-                        "ns": "http://www.w3.org/2000/svg",
-                        "children": [
-                          {
-                            "tag": "path",
-                            "ns": "http://www.w3.org/2000/svg"
-                          }
-                        ]
-                      }
-                    ]
-                  },
-                  {
-                    "text": "a"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><div><svg><path></path></svg></div>a</body></html>",
-        "noQuirksBodyHtml": "<div><svg><path></path></svg></div>a"
-      }
-    },
-    {
-      "data": "<div><svg><path></svg><path>",
-      "errors": [
-        "(1,5) expected-doctype-but-got-start-tag",
-        "(1,22) unexpected-end-tag",
-        "(1,28) expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "div": true,
-            "svg svg": true,
-            "svg path": true,
-            "path": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "div",
-                    "children": [
-                      {
-                        "tag": "svg",
-                        "ns": "http://www.w3.org/2000/svg",
-                        "children": [
-                          {
-                            "tag": "path",
-                            "ns": "http://www.w3.org/2000/svg"
-                          }
-                        ]
-                      },
-                      {
-                        "tag": "path"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><div><svg><path></path></svg><path></path></div></body></html>",
-        "noQuirksBodyHtml": "<div><svg><path></path></svg><path></path></div>"
-      }
-    },
-    {
-      "data": "<div><svg><path><foreignObject><math></div>a",
-      "errors": [
-        "(1,5) expected-doctype-but-got-start-tag",
-        "(1,43) unexpected-end-tag",
-        "(1,43) end-tag-too-early",
-        "(1,44) expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "div": true,
-            "svg svg": true,
-            "svg path": true,
-            "svg foreignObject": true,
-            "math math": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "div",
-                    "children": [
-                      {
-                        "tag": "svg",
-                        "ns": "http://www.w3.org/2000/svg",
-                        "children": [
-                          {
-                            "tag": "path",
-                            "ns": "http://www.w3.org/2000/svg",
-                            "children": [
-                              {
-                                "tag": "foreignObject",
-                                "ns": "http://www.w3.org/2000/svg",
-                                "children": [
-                                  {
-                                    "tag": "math",
-                                    "ns": "http://www.w3.org/1998/Math/MathML",
-                                    "children": [
-                                      {
-                                        "text": "a"
-                                      }
-                                    ]
-                                  }
-                                ]
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><div><svg><path><foreignObject><math>a</math></foreignObject></path></svg></div></body></html>",
-        "noQuirksBodyHtml": "<div><svg><path><foreignObject><math>a</math></foreignObject></path></svg></div>"
-      }
-    },
-    {
-      "data": "<div><svg><path><foreignObject><p></div>a",
-      "errors": [
-        "(1,5) expected-doctype-but-got-start-tag",
-        "(1,40) end-tag-too-early",
-        "(1,41) expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "div": true,
-            "svg svg": true,
-            "svg path": true,
-            "svg foreignObject": true,
-            "p": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "div",
-                    "children": [
-                      {
-                        "tag": "svg",
-                        "ns": "http://www.w3.org/2000/svg",
-                        "children": [
-                          {
-                            "tag": "path",
-                            "ns": "http://www.w3.org/2000/svg",
-                            "children": [
-                              {
-                                "tag": "foreignObject",
-                                "ns": "http://www.w3.org/2000/svg",
-                                "children": [
-                                  {
-                                    "tag": "p",
-                                    "children": [
-                                      {
-                                        "text": "a"
-                                      }
-                                    ]
-                                  }
-                                ]
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><div><svg><path><foreignObject><p>a</p></foreignObject></path></svg></div></body></html>",
-        "noQuirksBodyHtml": "<div><svg><path><foreignObject><p>a</p></foreignObject></path></svg></div>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><svg><desc><div><svg><ul>a",
-      "errors": [
-        "(1,40) unexpected-html-element-in-foreign-content",
-        "(1,41) expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "svg svg": true,
-            "svg desc": true,
-            "div": true,
-            "ul": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "svg",
-                    "ns": "http://www.w3.org/2000/svg",
-                    "children": [
-                      {
-                        "tag": "desc",
-                        "ns": "http://www.w3.org/2000/svg",
-                        "children": [
-                          {
-                            "tag": "div",
-                            "children": [
-                              {
-                                "tag": "svg",
-                                "ns": "http://www.w3.org/2000/svg"
-                              },
-                              {
-                                "tag": "ul",
-                                "children": [
-                                  {
-                                    "text": "a"
-                                  }
-                                ]
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><svg><desc><div><svg></svg><ul>a</ul></div></desc></svg></body></html>",
-        "noQuirksBodyHtml": "<svg><desc><div><svg><ul>a</ul></svg></div></desc></svg>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><svg><desc><svg><ul>a",
-      "errors": [
-        "(1,35) unexpected-html-element-in-foreign-content",
-        "(1,36) expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "svg svg": true,
-            "svg desc": true,
-            "ul": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "svg",
-                    "ns": "http://www.w3.org/2000/svg",
-                    "children": [
-                      {
-                        "tag": "desc",
-                        "ns": "http://www.w3.org/2000/svg",
-                        "children": [
-                          {
-                            "tag": "svg",
-                            "ns": "http://www.w3.org/2000/svg"
-                          },
-                          {
-                            "tag": "ul",
-                            "children": [
-                              {
-                                "text": "a"
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><svg><desc><svg></svg><ul>a</ul></desc></svg></body></html>",
-        "noQuirksBodyHtml": "<svg><desc><svg><ul>a</ul></svg></desc></svg>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><p><svg><desc><p>",
-      "errors": [
-        "(1,32) expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "p": true,
-            "svg svg": true,
-            "svg desc": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "p",
-                    "children": [
-                      {
-                        "tag": "svg",
-                        "ns": "http://www.w3.org/2000/svg",
-                        "children": [
-                          {
-                            "tag": "desc",
-                            "ns": "http://www.w3.org/2000/svg",
-                            "children": [
-                              {
-                                "tag": "p"
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><p><svg><desc><p></p></desc></svg></p></body></html>",
-        "noQuirksBodyHtml": "<p><svg><desc><p></p></desc></svg></p>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><p><svg><title><p>",
-      "errors": [
-        "(1,33) expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "p": true,
-            "svg svg": true,
-            "svg title": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "p",
-                    "children": [
-                      {
-                        "tag": "svg",
-                        "ns": "http://www.w3.org/2000/svg",
-                        "children": [
-                          {
-                            "tag": "title",
-                            "ns": "http://www.w3.org/2000/svg",
-                            "children": [
-                              {
-                                "tag": "p"
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><p><svg><title><p></p></title></svg></p></body></html>",
-        "noQuirksBodyHtml": "<p><svg><title><p></p></title></svg></p>"
-      }
-    },
-    {
-      "data": "<div><svg><path><foreignObject><p></foreignObject><p>",
-      "errors": [
-        "(1,5) expected-doctype-but-got-start-tag",
-        "(1,50) unexpected-end-tag",
-        "(1,53) expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "div": true,
-            "svg svg": true,
-            "svg path": true,
-            "svg foreignObject": true,
-            "p": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "div",
-                    "children": [
-                      {
-                        "tag": "svg",
-                        "ns": "http://www.w3.org/2000/svg",
-                        "children": [
-                          {
-                            "tag": "path",
-                            "ns": "http://www.w3.org/2000/svg",
-                            "children": [
-                              {
-                                "tag": "foreignObject",
-                                "ns": "http://www.w3.org/2000/svg",
-                                "children": [
-                                  {
-                                    "tag": "p"
-                                  },
-                                  {
-                                    "tag": "p"
-                                  }
-                                ]
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><div><svg><path><foreignObject><p></p><p></p></foreignObject></path></svg></div></body></html>",
-        "noQuirksBodyHtml": "<div><svg><path><foreignObject><p></p><p></p></foreignObject></path></svg></div>"
-      }
-    },
-    {
-      "data": "<math><mi><div><object><div><span></span></div></object></div></mi><mi>",
-      "errors": [
-        "(1,6) expected-doctype-but-got-start-tag",
-        "(1,71) expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "math math": true,
-            "math mi": true,
-            "div": true,
-            "object": true,
-            "span": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "math",
-                    "ns": "http://www.w3.org/1998/Math/MathML",
-                    "children": [
-                      {
-                        "tag": "mi",
-                        "ns": "http://www.w3.org/1998/Math/MathML",
-                        "children": [
-                          {
-                            "tag": "div",
-                            "children": [
-                              {
-                                "tag": "object",
-                                "children": [
-                                  {
-                                    "tag": "div",
-                                    "children": [
-                                      {
-                                        "tag": "span"
-                                      }
-                                    ]
-                                  }
-                                ]
-                              }
-                            ]
-                          }
-                        ]
-                      },
-                      {
-                        "tag": "mi",
-                        "ns": "http://www.w3.org/1998/Math/MathML"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><math><mi><div><object><div><span></span></div></object></div></mi><mi></mi></math></body></html>",
-        "noQuirksBodyHtml": "<math><mi><div><object><div><span></span></div></object></div></mi><mi></mi></math>"
-      }
-    },
-    {
-      "data": "<math><mi><svg><foreignObject><div><div></div></div></foreignObject></svg></mi><mi>",
-      "errors": [
-        "(1,6) expected-doctype-but-got-start-tag",
-        "(1,83) expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "math math": true,
-            "math mi": true,
-            "svg svg": true,
-            "svg foreignObject": true,
-            "div": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "math",
-                    "ns": "http://www.w3.org/1998/Math/MathML",
-                    "children": [
-                      {
-                        "tag": "mi",
-                        "ns": "http://www.w3.org/1998/Math/MathML",
-                        "children": [
-                          {
-                            "tag": "svg",
-                            "ns": "http://www.w3.org/2000/svg",
-                            "children": [
-                              {
-                                "tag": "foreignObject",
-                                "ns": "http://www.w3.org/2000/svg",
-                                "children": [
-                                  {
-                                    "tag": "div",
-                                    "children": [
-                                      {
-                                        "tag": "div"
-                                      }
-                                    ]
-                                  }
-                                ]
-                              }
-                            ]
-                          }
-                        ]
-                      },
-                      {
-                        "tag": "mi",
-                        "ns": "http://www.w3.org/1998/Math/MathML"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><math><mi><svg><foreignObject><div><div></div></div></foreignObject></svg></mi><mi></mi></math></body></html>",
-        "noQuirksBodyHtml": "<math><mi><svg><foreignObject><div><div></div></div></foreignObject></svg></mi><mi></mi></math>"
-      }
-    },
-    {
-      "data": "<svg><script></script><path>",
-      "errors": [
-        "(1,5) expected-doctype-but-got-start-tag",
-        "(1,28) expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "svg svg": true,
-            "svg script": true,
-            "svg path": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "svg",
-                    "ns": "http://www.w3.org/2000/svg",
-                    "children": [
-                      {
-                        "tag": "script",
-                        "ns": "http://www.w3.org/2000/svg"
-                      },
-                      {
-                        "tag": "path",
-                        "ns": "http://www.w3.org/2000/svg"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><svg><script></script><path></path></svg></body></html>",
-        "noQuirksBodyHtml": "<svg><script></script><path></path></svg>"
-      }
-    },
-    {
-      "data": "<table><svg></svg><tr>",
-      "errors": [
-        "(1,7) expected-doctype-but-got-start-tag",
-        "(1,12) unexpected-start-tag-implies-table-voodoo",
-        "(1,22) eof-in-table"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "svg svg": true,
-            "table": true,
-            "tbody": true,
-            "tr": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "svg",
-                    "ns": "http://www.w3.org/2000/svg"
-                  },
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "tbody",
-                        "children": [
-                          {
-                            "tag": "tr"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><svg></svg><table><tbody><tr></tr></tbody></table></body></html>",
-        "noQuirksBodyHtml": "<svg></svg><table><tbody><tr></tr></tbody></table>"
-      }
-    },
-    {
-      "data": "<math><mi><mglyph>",
-      "errors": [
-        "(1,6) expected-doctype-but-got-start-tag",
-        "(1,18) expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "math math": true,
-            "math mi": true,
-            "math mglyph": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "math",
-                    "ns": "http://www.w3.org/1998/Math/MathML",
-                    "children": [
-                      {
-                        "tag": "mi",
-                        "ns": "http://www.w3.org/1998/Math/MathML",
-                        "children": [
-                          {
-                            "tag": "mglyph",
-                            "ns": "http://www.w3.org/1998/Math/MathML"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><math><mi><mglyph></mglyph></mi></math></body></html>",
-        "noQuirksBodyHtml": "<math><mi><mglyph></mglyph></mi></math>"
-      }
-    },
-    {
-      "data": "<math><mi><malignmark>",
-      "errors": [
-        "(1,6) expected-doctype-but-got-start-tag",
-        "(1,22) expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "math math": true,
-            "math mi": true,
-            "math malignmark": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "math",
-                    "ns": "http://www.w3.org/1998/Math/MathML",
-                    "children": [
-                      {
-                        "tag": "mi",
-                        "ns": "http://www.w3.org/1998/Math/MathML",
-                        "children": [
-                          {
-                            "tag": "malignmark",
-                            "ns": "http://www.w3.org/1998/Math/MathML"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><math><mi><malignmark></malignmark></mi></math></body></html>",
-        "noQuirksBodyHtml": "<math><mi><malignmark></malignmark></mi></math>"
-      }
-    },
-    {
-      "data": "<math><mo><mglyph>",
-      "errors": [
-        "(1,6) expected-doctype-but-got-start-tag",
-        "(1,18) expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "math math": true,
-            "math mo": true,
-            "math mglyph": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "math",
-                    "ns": "http://www.w3.org/1998/Math/MathML",
-                    "children": [
-                      {
-                        "tag": "mo",
-                        "ns": "http://www.w3.org/1998/Math/MathML",
-                        "children": [
-                          {
-                            "tag": "mglyph",
-                            "ns": "http://www.w3.org/1998/Math/MathML"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><math><mo><mglyph></mglyph></mo></math></body></html>",
-        "noQuirksBodyHtml": "<math><mo><mglyph></mglyph></mo></math>"
-      }
-    },
-    {
-      "data": "<math><mo><malignmark>",
-      "errors": [
-        "(1,6) expected-doctype-but-got-start-tag",
-        "(1,22) expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "math math": true,
-            "math mo": true,
-            "math malignmark": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "math",
-                    "ns": "http://www.w3.org/1998/Math/MathML",
-                    "children": [
-                      {
-                        "tag": "mo",
-                        "ns": "http://www.w3.org/1998/Math/MathML",
-                        "children": [
-                          {
-                            "tag": "malignmark",
-                            "ns": "http://www.w3.org/1998/Math/MathML"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><math><mo><malignmark></malignmark></mo></math></body></html>",
-        "noQuirksBodyHtml": "<math><mo><malignmark></malignmark></mo></math>"
-      }
-    },
-    {
-      "data": "<math><mn><mglyph>",
-      "errors": [
-        "(1,6) expected-doctype-but-got-start-tag",
-        "(1,18) expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "math math": true,
-            "math mn": true,
-            "math mglyph": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "math",
-                    "ns": "http://www.w3.org/1998/Math/MathML",
-                    "children": [
-                      {
-                        "tag": "mn",
-                        "ns": "http://www.w3.org/1998/Math/MathML",
-                        "children": [
-                          {
-                            "tag": "mglyph",
-                            "ns": "http://www.w3.org/1998/Math/MathML"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><math><mn><mglyph></mglyph></mn></math></body></html>",
-        "noQuirksBodyHtml": "<math><mn><mglyph></mglyph></mn></math>"
-      }
-    },
-    {
-      "data": "<math><mn><malignmark>",
-      "errors": [
-        "(1,6) expected-doctype-but-got-start-tag",
-        "(1,22) expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "math math": true,
-            "math mn": true,
-            "math malignmark": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "math",
-                    "ns": "http://www.w3.org/1998/Math/MathML",
-                    "children": [
-                      {
-                        "tag": "mn",
-                        "ns": "http://www.w3.org/1998/Math/MathML",
-                        "children": [
-                          {
-                            "tag": "malignmark",
-                            "ns": "http://www.w3.org/1998/Math/MathML"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><math><mn><malignmark></malignmark></mn></math></body></html>",
-        "noQuirksBodyHtml": "<math><mn><malignmark></malignmark></mn></math>"
-      }
-    },
-    {
-      "data": "<math><ms><mglyph>",
-      "errors": [
-        "(1,6) expected-doctype-but-got-start-tag",
-        "(1,18) expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "math math": true,
-            "math ms": true,
-            "math mglyph": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "math",
-                    "ns": "http://www.w3.org/1998/Math/MathML",
-                    "children": [
-                      {
-                        "tag": "ms",
-                        "ns": "http://www.w3.org/1998/Math/MathML",
-                        "children": [
-                          {
-                            "tag": "mglyph",
-                            "ns": "http://www.w3.org/1998/Math/MathML"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><math><ms><mglyph></mglyph></ms></math></body></html>",
-        "noQuirksBodyHtml": "<math><ms><mglyph></mglyph></ms></math>"
-      }
-    },
-    {
-      "data": "<math><ms><malignmark>",
-      "errors": [
-        "(1,6) expected-doctype-but-got-start-tag",
-        "(1,22) expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "math math": true,
-            "math ms": true,
-            "math malignmark": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "math",
-                    "ns": "http://www.w3.org/1998/Math/MathML",
-                    "children": [
-                      {
-                        "tag": "ms",
-                        "ns": "http://www.w3.org/1998/Math/MathML",
-                        "children": [
-                          {
-                            "tag": "malignmark",
-                            "ns": "http://www.w3.org/1998/Math/MathML"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><math><ms><malignmark></malignmark></ms></math></body></html>",
-        "noQuirksBodyHtml": "<math><ms><malignmark></malignmark></ms></math>"
-      }
-    },
-    {
-      "data": "<math><mtext><mglyph>",
-      "errors": [
-        "(1,6) expected-doctype-but-got-start-tag",
-        "(1,21) expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "math math": true,
-            "math mtext": true,
-            "math mglyph": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "math",
-                    "ns": "http://www.w3.org/1998/Math/MathML",
-                    "children": [
-                      {
-                        "tag": "mtext",
-                        "ns": "http://www.w3.org/1998/Math/MathML",
-                        "children": [
-                          {
-                            "tag": "mglyph",
-                            "ns": "http://www.w3.org/1998/Math/MathML"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><math><mtext><mglyph></mglyph></mtext></math></body></html>",
-        "noQuirksBodyHtml": "<math><mtext><mglyph></mglyph></mtext></math>"
-      }
-    },
-    {
-      "data": "<math><mtext><malignmark>",
-      "errors": [
-        "(1,6) expected-doctype-but-got-start-tag",
-        "(1,25) expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "math math": true,
-            "math mtext": true,
-            "math malignmark": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "math",
-                    "ns": "http://www.w3.org/1998/Math/MathML",
-                    "children": [
-                      {
-                        "tag": "mtext",
-                        "ns": "http://www.w3.org/1998/Math/MathML",
-                        "children": [
-                          {
-                            "tag": "malignmark",
-                            "ns": "http://www.w3.org/1998/Math/MathML"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><math><mtext><malignmark></malignmark></mtext></math></body></html>",
-        "noQuirksBodyHtml": "<math><mtext><malignmark></malignmark></mtext></math>"
-      }
-    },
-    {
-      "data": "<math><annotation-xml><svg></svg></annotation-xml><mi>",
-      "errors": [
-        "(1,6) expected-doctype-but-got-start-tag",
-        "(1,54) expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "math math": true,
-            "math annotation-xml": true,
-            "svg svg": true,
-            "math mi": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "math",
-                    "ns": "http://www.w3.org/1998/Math/MathML",
-                    "children": [
-                      {
-                        "tag": "annotation-xml",
-                        "ns": "http://www.w3.org/1998/Math/MathML",
-                        "children": [
-                          {
-                            "tag": "svg",
-                            "ns": "http://www.w3.org/2000/svg"
-                          }
-                        ]
-                      },
-                      {
-                        "tag": "mi",
-                        "ns": "http://www.w3.org/1998/Math/MathML"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><math><annotation-xml><svg></svg></annotation-xml><mi></mi></math></body></html>",
-        "noQuirksBodyHtml": "<math><annotation-xml><svg></svg></annotation-xml><mi></mi></math>"
-      }
-    },
-    {
-      "data": "<math><annotation-xml><svg><foreignObject><div><math><mi></mi></math><span></span></div></foreignObject><path></path></svg></annotation-xml><mi>",
-      "errors": [
-        "(1,6) expected-doctype-but-got-start-tag",
-        "(1,144) expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "math math": true,
-            "math annotation-xml": true,
-            "svg svg": true,
-            "svg foreignObject": true,
-            "div": true,
-            "math mi": true,
-            "span": true,
-            "svg path": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "math",
-                    "ns": "http://www.w3.org/1998/Math/MathML",
-                    "children": [
-                      {
-                        "tag": "annotation-xml",
-                        "ns": "http://www.w3.org/1998/Math/MathML",
-                        "children": [
-                          {
-                            "tag": "svg",
-                            "ns": "http://www.w3.org/2000/svg",
-                            "children": [
-                              {
-                                "tag": "foreignObject",
-                                "ns": "http://www.w3.org/2000/svg",
-                                "children": [
-                                  {
-                                    "tag": "div",
-                                    "children": [
-                                      {
-                                        "tag": "math",
-                                        "ns": "http://www.w3.org/1998/Math/MathML",
-                                        "children": [
-                                          {
-                                            "tag": "mi",
-                                            "ns": "http://www.w3.org/1998/Math/MathML"
-                                          }
-                                        ]
-                                      },
-                                      {
-                                        "tag": "span"
-                                      }
-                                    ]
-                                  }
-                                ]
-                              },
-                              {
-                                "tag": "path",
-                                "ns": "http://www.w3.org/2000/svg"
-                              }
-                            ]
-                          }
-                        ]
-                      },
-                      {
-                        "tag": "mi",
-                        "ns": "http://www.w3.org/1998/Math/MathML"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><math><annotation-xml><svg><foreignObject><div><math><mi></mi></math><span></span></div></foreignObject><path></path></svg></annotation-xml><mi></mi></math></body></html>",
-        "noQuirksBodyHtml": "<math><annotation-xml><svg><foreignObject><div><math><mi></mi></math><span></span></div></foreignObject><path></path></svg></annotation-xml><mi></mi></math>"
-      }
-    },
-    {
-      "data": "<math><annotation-xml><svg><foreignObject><math><mi><svg></svg></mi><mo></mo></math><span></span></foreignObject><path></path></svg></annotation-xml><mi>",
-      "errors": [
-        "(1,6) expected-doctype-but-got-start-tag",
-        "(1,153) expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "math math": true,
-            "math annotation-xml": true,
-            "svg svg": true,
-            "svg foreignObject": true,
-            "math mi": true,
-            "math mo": true,
-            "span": true,
-            "svg path": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "math",
-                    "ns": "http://www.w3.org/1998/Math/MathML",
-                    "children": [
-                      {
-                        "tag": "annotation-xml",
-                        "ns": "http://www.w3.org/1998/Math/MathML",
-                        "children": [
-                          {
-                            "tag": "svg",
-                            "ns": "http://www.w3.org/2000/svg",
-                            "children": [
-                              {
-                                "tag": "foreignObject",
-                                "ns": "http://www.w3.org/2000/svg",
-                                "children": [
-                                  {
-                                    "tag": "math",
-                                    "ns": "http://www.w3.org/1998/Math/MathML",
-                                    "children": [
-                                      {
-                                        "tag": "mi",
-                                        "ns": "http://www.w3.org/1998/Math/MathML",
-                                        "children": [
-                                          {
-                                            "tag": "svg",
-                                            "ns": "http://www.w3.org/2000/svg"
-                                          }
-                                        ]
-                                      },
-                                      {
-                                        "tag": "mo",
-                                        "ns": "http://www.w3.org/1998/Math/MathML"
-                                      }
-                                    ]
-                                  },
-                                  {
-                                    "tag": "span"
-                                  }
-                                ]
-                              },
-                              {
-                                "tag": "path",
-                                "ns": "http://www.w3.org/2000/svg"
-                              }
-                            ]
-                          }
-                        ]
-                      },
-                      {
-                        "tag": "mi",
-                        "ns": "http://www.w3.org/1998/Math/MathML"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><math><annotation-xml><svg><foreignObject><math><mi><svg></svg></mi><mo></mo></math><span></span></foreignObject><path></path></svg></annotation-xml><mi></mi></math></body></html>",
-        "noQuirksBodyHtml": "<math><annotation-xml><svg><foreignObject><math><mi><svg></svg></mi><mo></mo></math><span></span></foreignObject><path></path></svg></annotation-xml><mi></mi></math>"
-      }
-    }
-  ],
-  "tests11.dat": [
-    {
-      "data": "<!DOCTYPE html><body><svg attributeName='' attributeType='' baseFrequency='' baseProfile='' calcMode='' clipPathUnits='' diffuseConstant='' edgeMode='' filterUnits='' glyphRef='' gradientTransform='' gradientUnits='' kernelMatrix='' kernelUnitLength='' keyPoints='' keySplines='' keyTimes='' lengthAdjust='' limitingConeAngle='' markerHeight='' markerUnits='' markerWidth='' maskContentUnits='' maskUnits='' numOctaves='' pathLength='' patternContentUnits='' patternTransform='' patternUnits='' pointsAtX='' pointsAtY='' pointsAtZ='' preserveAlpha='' preserveAspectRatio='' primitiveUnits='' refX='' refY='' repeatCount='' repeatDur='' requiredExtensions='' requiredFeatures='' specularConstant='' specularExponent='' spreadMethod='' startOffset='' stdDeviation='' stitchTiles='' surfaceScale='' systemLanguage='' tableValues='' targetX='' targetY='' textLength='' viewBox='' viewTarget='' xChannelSelector='' yChannelSelector='' zoomAndPan=''></svg>",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "svg svg": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "svg",
-                    "ns": "http://www.w3.org/2000/svg",
-                    "attrs": [
-                      {
-                        "name": "attributeName",
-                        "value": ""
-                      },
-                      {
-                        "name": "attributeType",
-                        "value": ""
-                      },
-                      {
-                        "name": "baseFrequency",
-                        "value": ""
-                      },
-                      {
-                        "name": "baseProfile",
-                        "value": ""
-                      },
-                      {
-                        "name": "calcMode",
-                        "value": ""
-                      },
-                      {
-                        "name": "clipPathUnits",
-                        "value": ""
-                      },
-                      {
-                        "name": "diffuseConstant",
-                        "value": ""
-                      },
-                      {
-                        "name": "edgeMode",
-                        "value": ""
-                      },
-                      {
-                        "name": "filterUnits",
-                        "value": ""
-                      },
-                      {
-                        "name": "glyphRef",
-                        "value": ""
-                      },
-                      {
-                        "name": "gradientTransform",
-                        "value": ""
-                      },
-                      {
-                        "name": "gradientUnits",
-                        "value": ""
-                      },
-                      {
-                        "name": "kernelMatrix",
-                        "value": ""
-                      },
-                      {
-                        "name": "kernelUnitLength",
-                        "value": ""
-                      },
-                      {
-                        "name": "keyPoints",
-                        "value": ""
-                      },
-                      {
-                        "name": "keySplines",
-                        "value": ""
-                      },
-                      {
-                        "name": "keyTimes",
-                        "value": ""
-                      },
-                      {
-                        "name": "lengthAdjust",
-                        "value": ""
-                      },
-                      {
-                        "name": "limitingConeAngle",
-                        "value": ""
-                      },
-                      {
-                        "name": "markerHeight",
-                        "value": ""
-                      },
-                      {
-                        "name": "markerUnits",
-                        "value": ""
-                      },
-                      {
-                        "name": "markerWidth",
-                        "value": ""
-                      },
-                      {
-                        "name": "maskContentUnits",
-                        "value": ""
-                      },
-                      {
-                        "name": "maskUnits",
-                        "value": ""
-                      },
-                      {
-                        "name": "numOctaves",
-                        "value": ""
-                      },
-                      {
-                        "name": "pathLength",
-                        "value": ""
-                      },
-                      {
-                        "name": "patternContentUnits",
-                        "value": ""
-                      },
-                      {
-                        "name": "patternTransform",
-                        "value": ""
-                      },
-                      {
-                        "name": "patternUnits",
-                        "value": ""
-                      },
-                      {
-                        "name": "pointsAtX",
-                        "value": ""
-                      },
-                      {
-                        "name": "pointsAtY",
-                        "value": ""
-                      },
-                      {
-                        "name": "pointsAtZ",
-                        "value": ""
-                      },
-                      {
-                        "name": "preserveAlpha",
-                        "value": ""
-                      },
-                      {
-                        "name": "preserveAspectRatio",
-                        "value": ""
-                      },
-                      {
-                        "name": "primitiveUnits",
-                        "value": ""
-                      },
-                      {
-                        "name": "refX",
-                        "value": ""
-                      },
-                      {
-                        "name": "refY",
-                        "value": ""
-                      },
-                      {
-                        "name": "repeatCount",
-                        "value": ""
-                      },
-                      {
-                        "name": "repeatDur",
-                        "value": ""
-                      },
-                      {
-                        "name": "requiredExtensions",
-                        "value": ""
-                      },
-                      {
-                        "name": "requiredFeatures",
-                        "value": ""
-                      },
-                      {
-                        "name": "specularConstant",
-                        "value": ""
-                      },
-                      {
-                        "name": "specularExponent",
-                        "value": ""
-                      },
-                      {
-                        "name": "spreadMethod",
-                        "value": ""
-                      },
-                      {
-                        "name": "startOffset",
-                        "value": ""
-                      },
-                      {
-                        "name": "stdDeviation",
-                        "value": ""
-                      },
-                      {
-                        "name": "stitchTiles",
-                        "value": ""
-                      },
-                      {
-                        "name": "surfaceScale",
-                        "value": ""
-                      },
-                      {
-                        "name": "systemLanguage",
-                        "value": ""
-                      },
-                      {
-                        "name": "tableValues",
-                        "value": ""
-                      },
-                      {
-                        "name": "targetX",
-                        "value": ""
-                      },
-                      {
-                        "name": "targetY",
-                        "value": ""
-                      },
-                      {
-                        "name": "textLength",
-                        "value": ""
-                      },
-                      {
-                        "name": "viewBox",
-                        "value": ""
-                      },
-                      {
-                        "name": "viewTarget",
-                        "value": ""
-                      },
-                      {
-                        "name": "xChannelSelector",
-                        "value": ""
-                      },
-                      {
-                        "name": "yChannelSelector",
-                        "value": ""
-                      },
-                      {
-                        "name": "zoomAndPan",
-                        "value": ""
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><svg attributeName=\"\" attributeType=\"\" baseFrequency=\"\" baseProfile=\"\" calcMode=\"\" clipPathUnits=\"\" diffuseConstant=\"\" edgeMode=\"\" filterUnits=\"\" glyphRef=\"\" gradientTransform=\"\" gradientUnits=\"\" kernelMatrix=\"\" kernelUnitLength=\"\" keyPoints=\"\" keySplines=\"\" keyTimes=\"\" lengthAdjust=\"\" limitingConeAngle=\"\" markerHeight=\"\" markerUnits=\"\" markerWidth=\"\" maskContentUnits=\"\" maskUnits=\"\" numOctaves=\"\" pathLength=\"\" patternContentUnits=\"\" patternTransform=\"\" patternUnits=\"\" pointsAtX=\"\" pointsAtY=\"\" pointsAtZ=\"\" preserveAlpha=\"\" preserveAspectRatio=\"\" primitiveUnits=\"\" refX=\"\" refY=\"\" repeatCount=\"\" repeatDur=\"\" requiredExtensions=\"\" requiredFeatures=\"\" specularConstant=\"\" specularExponent=\"\" spreadMethod=\"\" startOffset=\"\" stdDeviation=\"\" stitchTiles=\"\" surfaceScale=\"\" systemLanguage=\"\" tableValues=\"\" targetX=\"\" targetY=\"\" textLength=\"\" viewBox=\"\" viewTarget=\"\" xChannelSelector=\"\" yChannelSelector=\"\" zoomAndPan=\"\"></svg></body></html>",
-        "noQuirksBodyHtml": "<svg attributeName=\"\" attributeType=\"\" baseFrequency=\"\" baseProfile=\"\" calcMode=\"\" clipPathUnits=\"\" diffuseConstant=\"\" edgeMode=\"\" filterUnits=\"\" glyphRef=\"\" gradientTransform=\"\" gradientUnits=\"\" kernelMatrix=\"\" kernelUnitLength=\"\" keyPoints=\"\" keySplines=\"\" keyTimes=\"\" lengthAdjust=\"\" limitingConeAngle=\"\" markerHeight=\"\" markerUnits=\"\" markerWidth=\"\" maskContentUnits=\"\" maskUnits=\"\" numOctaves=\"\" pathLength=\"\" patternContentUnits=\"\" patternTransform=\"\" patternUnits=\"\" pointsAtX=\"\" pointsAtY=\"\" pointsAtZ=\"\" preserveAlpha=\"\" preserveAspectRatio=\"\" primitiveUnits=\"\" refX=\"\" refY=\"\" repeatCount=\"\" repeatDur=\"\" requiredExtensions=\"\" requiredFeatures=\"\" specularConstant=\"\" specularExponent=\"\" spreadMethod=\"\" startOffset=\"\" stdDeviation=\"\" stitchTiles=\"\" surfaceScale=\"\" systemLanguage=\"\" tableValues=\"\" targetX=\"\" targetY=\"\" textLength=\"\" viewBox=\"\" viewTarget=\"\" xChannelSelector=\"\" yChannelSelector=\"\" zoomAndPan=\"\"></svg>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><BODY><SVG ATTRIBUTENAME='' ATTRIBUTETYPE='' BASEFREQUENCY='' BASEPROFILE='' CALCMODE='' CLIPPATHUNITS='' DIFFUSECONSTANT='' EDGEMODE='' FILTERUNITS='' GLYPHREF='' GRADIENTTRANSFORM='' GRADIENTUNITS='' KERNELMATRIX='' KERNELUNITLENGTH='' KEYPOINTS='' KEYSPLINES='' KEYTIMES='' LENGTHADJUST='' LIMITINGCONEANGLE='' MARKERHEIGHT='' MARKERUNITS='' MARKERWIDTH='' MASKCONTENTUNITS='' MASKUNITS='' NUMOCTAVES='' PATHLENGTH='' PATTERNCONTENTUNITS='' PATTERNTRANSFORM='' PATTERNUNITS='' POINTSATX='' POINTSATY='' POINTSATZ='' PRESERVEALPHA='' PRESERVEASPECTRATIO='' PRIMITIVEUNITS='' REFX='' REFY='' REPEATCOUNT='' REPEATDUR='' REQUIREDEXTENSIONS='' REQUIREDFEATURES='' SPECULARCONSTANT='' SPECULAREXPONENT='' SPREADMETHOD='' STARTOFFSET='' STDDEVIATION='' STITCHTILES='' SURFACESCALE='' SYSTEMLANGUAGE='' TABLEVALUES='' TARGETX='' TARGETY='' TEXTLENGTH='' VIEWBOX='' VIEWTARGET='' XCHANNELSELECTOR='' YCHANNELSELECTOR='' ZOOMANDPAN=''></SVG>",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "svg svg": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "svg",
-                    "ns": "http://www.w3.org/2000/svg",
-                    "attrs": [
-                      {
-                        "name": "attributeName",
-                        "value": ""
-                      },
-                      {
-                        "name": "attributeType",
-                        "value": ""
-                      },
-                      {
-                        "name": "baseFrequency",
-                        "value": ""
-                      },
-                      {
-                        "name": "baseProfile",
-                        "value": ""
-                      },
-                      {
-                        "name": "calcMode",
-                        "value": ""
-                      },
-                      {
-                        "name": "clipPathUnits",
-                        "value": ""
-                      },
-                      {
-                        "name": "diffuseConstant",
-                        "value": ""
-                      },
-                      {
-                        "name": "edgeMode",
-                        "value": ""
-                      },
-                      {
-                        "name": "filterUnits",
-                        "value": ""
-                      },
-                      {
-                        "name": "glyphRef",
-                        "value": ""
-                      },
-                      {
-                        "name": "gradientTransform",
-                        "value": ""
-                      },
-                      {
-                        "name": "gradientUnits",
-                        "value": ""
-                      },
-                      {
-                        "name": "kernelMatrix",
-                        "value": ""
-                      },
-                      {
-                        "name": "kernelUnitLength",
-                        "value": ""
-                      },
-                      {
-                        "name": "keyPoints",
-                        "value": ""
-                      },
-                      {
-                        "name": "keySplines",
-                        "value": ""
-                      },
-                      {
-                        "name": "keyTimes",
-                        "value": ""
-                      },
-                      {
-                        "name": "lengthAdjust",
-                        "value": ""
-                      },
-                      {
-                        "name": "limitingConeAngle",
-                        "value": ""
-                      },
-                      {
-                        "name": "markerHeight",
-                        "value": ""
-                      },
-                      {
-                        "name": "markerUnits",
-                        "value": ""
-                      },
-                      {
-                        "name": "markerWidth",
-                        "value": ""
-                      },
-                      {
-                        "name": "maskContentUnits",
-                        "value": ""
-                      },
-                      {
-                        "name": "maskUnits",
-                        "value": ""
-                      },
-                      {
-                        "name": "numOctaves",
-                        "value": ""
-                      },
-                      {
-                        "name": "pathLength",
-                        "value": ""
-                      },
-                      {
-                        "name": "patternContentUnits",
-                        "value": ""
-                      },
-                      {
-                        "name": "patternTransform",
-                        "value": ""
-                      },
-                      {
-                        "name": "patternUnits",
-                        "value": ""
-                      },
-                      {
-                        "name": "pointsAtX",
-                        "value": ""
-                      },
-                      {
-                        "name": "pointsAtY",
-                        "value": ""
-                      },
-                      {
-                        "name": "pointsAtZ",
-                        "value": ""
-                      },
-                      {
-                        "name": "preserveAlpha",
-                        "value": ""
-                      },
-                      {
-                        "name": "preserveAspectRatio",
-                        "value": ""
-                      },
-                      {
-                        "name": "primitiveUnits",
-                        "value": ""
-                      },
-                      {
-                        "name": "refX",
-                        "value": ""
-                      },
-                      {
-                        "name": "refY",
-                        "value": ""
-                      },
-                      {
-                        "name": "repeatCount",
-                        "value": ""
-                      },
-                      {
-                        "name": "repeatDur",
-                        "value": ""
-                      },
-                      {
-                        "name": "requiredExtensions",
-                        "value": ""
-                      },
-                      {
-                        "name": "requiredFeatures",
-                        "value": ""
-                      },
-                      {
-                        "name": "specularConstant",
-                        "value": ""
-                      },
-                      {
-                        "name": "specularExponent",
-                        "value": ""
-                      },
-                      {
-                        "name": "spreadMethod",
-                        "value": ""
-                      },
-                      {
-                        "name": "startOffset",
-                        "value": ""
-                      },
-                      {
-                        "name": "stdDeviation",
-                        "value": ""
-                      },
-                      {
-                        "name": "stitchTiles",
-                        "value": ""
-                      },
-                      {
-                        "name": "surfaceScale",
-                        "value": ""
-                      },
-                      {
-                        "name": "systemLanguage",
-                        "value": ""
-                      },
-                      {
-                        "name": "tableValues",
-                        "value": ""
-                      },
-                      {
-                        "name": "targetX",
-                        "value": ""
-                      },
-                      {
-                        "name": "targetY",
-                        "value": ""
-                      },
-                      {
-                        "name": "textLength",
-                        "value": ""
-                      },
-                      {
-                        "name": "viewBox",
-                        "value": ""
-                      },
-                      {
-                        "name": "viewTarget",
-                        "value": ""
-                      },
-                      {
-                        "name": "xChannelSelector",
-                        "value": ""
-                      },
-                      {
-                        "name": "yChannelSelector",
-                        "value": ""
-                      },
-                      {
-                        "name": "zoomAndPan",
-                        "value": ""
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><svg attributeName=\"\" attributeType=\"\" baseFrequency=\"\" baseProfile=\"\" calcMode=\"\" clipPathUnits=\"\" diffuseConstant=\"\" edgeMode=\"\" filterUnits=\"\" glyphRef=\"\" gradientTransform=\"\" gradientUnits=\"\" kernelMatrix=\"\" kernelUnitLength=\"\" keyPoints=\"\" keySplines=\"\" keyTimes=\"\" lengthAdjust=\"\" limitingConeAngle=\"\" markerHeight=\"\" markerUnits=\"\" markerWidth=\"\" maskContentUnits=\"\" maskUnits=\"\" numOctaves=\"\" pathLength=\"\" patternContentUnits=\"\" patternTransform=\"\" patternUnits=\"\" pointsAtX=\"\" pointsAtY=\"\" pointsAtZ=\"\" preserveAlpha=\"\" preserveAspectRatio=\"\" primitiveUnits=\"\" refX=\"\" refY=\"\" repeatCount=\"\" repeatDur=\"\" requiredExtensions=\"\" requiredFeatures=\"\" specularConstant=\"\" specularExponent=\"\" spreadMethod=\"\" startOffset=\"\" stdDeviation=\"\" stitchTiles=\"\" surfaceScale=\"\" systemLanguage=\"\" tableValues=\"\" targetX=\"\" targetY=\"\" textLength=\"\" viewBox=\"\" viewTarget=\"\" xChannelSelector=\"\" yChannelSelector=\"\" zoomAndPan=\"\"></svg></body></html>",
-        "noQuirksBodyHtml": "<svg attributeName=\"\" attributeType=\"\" baseFrequency=\"\" baseProfile=\"\" calcMode=\"\" clipPathUnits=\"\" diffuseConstant=\"\" edgeMode=\"\" filterUnits=\"\" glyphRef=\"\" gradientTransform=\"\" gradientUnits=\"\" kernelMatrix=\"\" kernelUnitLength=\"\" keyPoints=\"\" keySplines=\"\" keyTimes=\"\" lengthAdjust=\"\" limitingConeAngle=\"\" markerHeight=\"\" markerUnits=\"\" markerWidth=\"\" maskContentUnits=\"\" maskUnits=\"\" numOctaves=\"\" pathLength=\"\" patternContentUnits=\"\" patternTransform=\"\" patternUnits=\"\" pointsAtX=\"\" pointsAtY=\"\" pointsAtZ=\"\" preserveAlpha=\"\" preserveAspectRatio=\"\" primitiveUnits=\"\" refX=\"\" refY=\"\" repeatCount=\"\" repeatDur=\"\" requiredExtensions=\"\" requiredFeatures=\"\" specularConstant=\"\" specularExponent=\"\" spreadMethod=\"\" startOffset=\"\" stdDeviation=\"\" stitchTiles=\"\" surfaceScale=\"\" systemLanguage=\"\" tableValues=\"\" targetX=\"\" targetY=\"\" textLength=\"\" viewBox=\"\" viewTarget=\"\" xChannelSelector=\"\" yChannelSelector=\"\" zoomAndPan=\"\"></svg>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><body><svg attributename='' attributetype='' basefrequency='' baseprofile='' calcmode='' clippathunits='' diffuseconstant='' edgemode='' filterunits='' filterres='' glyphref='' gradienttransform='' gradientunits='' kernelmatrix='' kernelunitlength='' keypoints='' keysplines='' keytimes='' lengthadjust='' limitingconeangle='' markerheight='' markerunits='' markerwidth='' maskcontentunits='' maskunits='' numoctaves='' pathlength='' patterncontentunits='' patterntransform='' patternunits='' pointsatx='' pointsaty='' pointsatz='' preservealpha='' preserveaspectratio='' primitiveunits='' refx='' refy='' repeatcount='' repeatdur='' requiredextensions='' requiredfeatures='' specularconstant='' specularexponent='' spreadmethod='' startoffset='' stddeviation='' stitchtiles='' surfacescale='' systemlanguage='' tablevalues='' targetx='' targety='' textlength='' viewbox='' viewtarget='' xchannelselector='' ychannelselector='' zoomandpan=''></svg>",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "svg svg": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "svg",
-                    "ns": "http://www.w3.org/2000/svg",
-                    "attrs": [
-                      {
-                        "name": "attributeName",
-                        "value": ""
-                      },
-                      {
-                        "name": "attributeType",
-                        "value": ""
-                      },
-                      {
-                        "name": "baseFrequency",
-                        "value": ""
-                      },
-                      {
-                        "name": "baseProfile",
-                        "value": ""
-                      },
-                      {
-                        "name": "calcMode",
-                        "value": ""
-                      },
-                      {
-                        "name": "clipPathUnits",
-                        "value": ""
-                      },
-                      {
-                        "name": "diffuseConstant",
-                        "value": ""
-                      },
-                      {
-                        "name": "edgeMode",
-                        "value": ""
-                      },
-                      {
-                        "name": "filterUnits",
-                        "value": ""
-                      },
-                      {
-                        "name": "filterres",
-                        "value": ""
-                      },
-                      {
-                        "name": "glyphRef",
-                        "value": ""
-                      },
-                      {
-                        "name": "gradientTransform",
-                        "value": ""
-                      },
-                      {
-                        "name": "gradientUnits",
-                        "value": ""
-                      },
-                      {
-                        "name": "kernelMatrix",
-                        "value": ""
-                      },
-                      {
-                        "name": "kernelUnitLength",
-                        "value": ""
-                      },
-                      {
-                        "name": "keyPoints",
-                        "value": ""
-                      },
-                      {
-                        "name": "keySplines",
-                        "value": ""
-                      },
-                      {
-                        "name": "keyTimes",
-                        "value": ""
-                      },
-                      {
-                        "name": "lengthAdjust",
-                        "value": ""
-                      },
-                      {
-                        "name": "limitingConeAngle",
-                        "value": ""
-                      },
-                      {
-                        "name": "markerHeight",
-                        "value": ""
-                      },
-                      {
-                        "name": "markerUnits",
-                        "value": ""
-                      },
-                      {
-                        "name": "markerWidth",
-                        "value": ""
-                      },
-                      {
-                        "name": "maskContentUnits",
-                        "value": ""
-                      },
-                      {
-                        "name": "maskUnits",
-                        "value": ""
-                      },
-                      {
-                        "name": "numOctaves",
-                        "value": ""
-                      },
-                      {
-                        "name": "pathLength",
-                        "value": ""
-                      },
-                      {
-                        "name": "patternContentUnits",
-                        "value": ""
-                      },
-                      {
-                        "name": "patternTransform",
-                        "value": ""
-                      },
-                      {
-                        "name": "patternUnits",
-                        "value": ""
-                      },
-                      {
-                        "name": "pointsAtX",
-                        "value": ""
-                      },
-                      {
-                        "name": "pointsAtY",
-                        "value": ""
-                      },
-                      {
-                        "name": "pointsAtZ",
-                        "value": ""
-                      },
-                      {
-                        "name": "preserveAlpha",
-                        "value": ""
-                      },
-                      {
-                        "name": "preserveAspectRatio",
-                        "value": ""
-                      },
-                      {
-                        "name": "primitiveUnits",
-                        "value": ""
-                      },
-                      {
-                        "name": "refX",
-                        "value": ""
-                      },
-                      {
-                        "name": "refY",
-                        "value": ""
-                      },
-                      {
-                        "name": "repeatCount",
-                        "value": ""
-                      },
-                      {
-                        "name": "repeatDur",
-                        "value": ""
-                      },
-                      {
-                        "name": "requiredExtensions",
-                        "value": ""
-                      },
-                      {
-                        "name": "requiredFeatures",
-                        "value": ""
-                      },
-                      {
-                        "name": "specularConstant",
-                        "value": ""
-                      },
-                      {
-                        "name": "specularExponent",
-                        "value": ""
-                      },
-                      {
-                        "name": "spreadMethod",
-                        "value": ""
-                      },
-                      {
-                        "name": "startOffset",
-                        "value": ""
-                      },
-                      {
-                        "name": "stdDeviation",
-                        "value": ""
-                      },
-                      {
-                        "name": "stitchTiles",
-                        "value": ""
-                      },
-                      {
-                        "name": "surfaceScale",
-                        "value": ""
-                      },
-                      {
-                        "name": "systemLanguage",
-                        "value": ""
-                      },
-                      {
-                        "name": "tableValues",
-                        "value": ""
-                      },
-                      {
-                        "name": "targetX",
-                        "value": ""
-                      },
-                      {
-                        "name": "targetY",
-                        "value": ""
-                      },
-                      {
-                        "name": "textLength",
-                        "value": ""
-                      },
-                      {
-                        "name": "viewBox",
-                        "value": ""
-                      },
-                      {
-                        "name": "viewTarget",
-                        "value": ""
-                      },
-                      {
-                        "name": "xChannelSelector",
-                        "value": ""
-                      },
-                      {
-                        "name": "yChannelSelector",
-                        "value": ""
-                      },
-                      {
-                        "name": "zoomAndPan",
-                        "value": ""
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><svg attributeName=\"\" attributeType=\"\" baseFrequency=\"\" baseProfile=\"\" calcMode=\"\" clipPathUnits=\"\" diffuseConstant=\"\" edgeMode=\"\" filterUnits=\"\" filterres=\"\" glyphRef=\"\" gradientTransform=\"\" gradientUnits=\"\" kernelMatrix=\"\" kernelUnitLength=\"\" keyPoints=\"\" keySplines=\"\" keyTimes=\"\" lengthAdjust=\"\" limitingConeAngle=\"\" markerHeight=\"\" markerUnits=\"\" markerWidth=\"\" maskContentUnits=\"\" maskUnits=\"\" numOctaves=\"\" pathLength=\"\" patternContentUnits=\"\" patternTransform=\"\" patternUnits=\"\" pointsAtX=\"\" pointsAtY=\"\" pointsAtZ=\"\" preserveAlpha=\"\" preserveAspectRatio=\"\" primitiveUnits=\"\" refX=\"\" refY=\"\" repeatCount=\"\" repeatDur=\"\" requiredExtensions=\"\" requiredFeatures=\"\" specularConstant=\"\" specularExponent=\"\" spreadMethod=\"\" startOffset=\"\" stdDeviation=\"\" stitchTiles=\"\" surfaceScale=\"\" systemLanguage=\"\" tableValues=\"\" targetX=\"\" targetY=\"\" textLength=\"\" viewBox=\"\" viewTarget=\"\" xChannelSelector=\"\" yChannelSelector=\"\" zoomAndPan=\"\"></svg></body></html>",
-        "noQuirksBodyHtml": "<svg attributeName=\"\" attributeType=\"\" baseFrequency=\"\" baseProfile=\"\" calcMode=\"\" clipPathUnits=\"\" diffuseConstant=\"\" edgeMode=\"\" filterUnits=\"\" filterres=\"\" glyphRef=\"\" gradientTransform=\"\" gradientUnits=\"\" kernelMatrix=\"\" kernelUnitLength=\"\" keyPoints=\"\" keySplines=\"\" keyTimes=\"\" lengthAdjust=\"\" limitingConeAngle=\"\" markerHeight=\"\" markerUnits=\"\" markerWidth=\"\" maskContentUnits=\"\" maskUnits=\"\" numOctaves=\"\" pathLength=\"\" patternContentUnits=\"\" patternTransform=\"\" patternUnits=\"\" pointsAtX=\"\" pointsAtY=\"\" pointsAtZ=\"\" preserveAlpha=\"\" preserveAspectRatio=\"\" primitiveUnits=\"\" refX=\"\" refY=\"\" repeatCount=\"\" repeatDur=\"\" requiredExtensions=\"\" requiredFeatures=\"\" specularConstant=\"\" specularExponent=\"\" spreadMethod=\"\" startOffset=\"\" stdDeviation=\"\" stitchTiles=\"\" surfaceScale=\"\" systemLanguage=\"\" tableValues=\"\" targetX=\"\" targetY=\"\" textLength=\"\" viewBox=\"\" viewTarget=\"\" xChannelSelector=\"\" yChannelSelector=\"\" zoomAndPan=\"\"></svg>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><body><math attributeName='' attributeType='' baseFrequency='' baseProfile='' calcMode='' clipPathUnits='' diffuseConstant='' edgeMode='' filterUnits='' glyphRef='' gradientTransform='' gradientUnits='' kernelMatrix='' kernelUnitLength='' keyPoints='' keySplines='' keyTimes='' lengthAdjust='' limitingConeAngle='' markerHeight='' markerUnits='' markerWidth='' maskContentUnits='' maskUnits='' numOctaves='' pathLength='' patternContentUnits='' patternTransform='' patternUnits='' pointsAtX='' pointsAtY='' pointsAtZ='' preserveAlpha='' preserveAspectRatio='' primitiveUnits='' refX='' refY='' repeatCount='' repeatDur='' requiredExtensions='' requiredFeatures='' specularConstant='' specularExponent='' spreadMethod='' startOffset='' stdDeviation='' stitchTiles='' surfaceScale='' systemLanguage='' tableValues='' targetX='' targetY='' textLength='' viewBox='' viewTarget='' xChannelSelector='' yChannelSelector='' zoomAndPan=''></math>",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "math math": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "math",
-                    "ns": "http://www.w3.org/1998/Math/MathML",
-                    "attrs": [
-                      {
-                        "name": "attributename",
-                        "value": ""
-                      },
-                      {
-                        "name": "attributetype",
-                        "value": ""
-                      },
-                      {
-                        "name": "basefrequency",
-                        "value": ""
-                      },
-                      {
-                        "name": "baseprofile",
-                        "value": ""
-                      },
-                      {
-                        "name": "calcmode",
-                        "value": ""
-                      },
-                      {
-                        "name": "clippathunits",
-                        "value": ""
-                      },
-                      {
-                        "name": "diffuseconstant",
-                        "value": ""
-                      },
-                      {
-                        "name": "edgemode",
-                        "value": ""
-                      },
-                      {
-                        "name": "filterunits",
-                        "value": ""
-                      },
-                      {
-                        "name": "glyphref",
-                        "value": ""
-                      },
-                      {
-                        "name": "gradienttransform",
-                        "value": ""
-                      },
-                      {
-                        "name": "gradientunits",
-                        "value": ""
-                      },
-                      {
-                        "name": "kernelmatrix",
-                        "value": ""
-                      },
-                      {
-                        "name": "kernelunitlength",
-                        "value": ""
-                      },
-                      {
-                        "name": "keypoints",
-                        "value": ""
-                      },
-                      {
-                        "name": "keysplines",
-                        "value": ""
-                      },
-                      {
-                        "name": "keytimes",
-                        "value": ""
-                      },
-                      {
-                        "name": "lengthadjust",
-                        "value": ""
-                      },
-                      {
-                        "name": "limitingconeangle",
-                        "value": ""
-                      },
-                      {
-                        "name": "markerheight",
-                        "value": ""
-                      },
-                      {
-                        "name": "markerunits",
-                        "value": ""
-                      },
-                      {
-                        "name": "markerwidth",
-                        "value": ""
-                      },
-                      {
-                        "name": "maskcontentunits",
-                        "value": ""
-                      },
-                      {
-                        "name": "maskunits",
-                        "value": ""
-                      },
-                      {
-                        "name": "numoctaves",
-                        "value": ""
-                      },
-                      {
-                        "name": "pathlength",
-                        "value": ""
-                      },
-                      {
-                        "name": "patterncontentunits",
-                        "value": ""
-                      },
-                      {
-                        "name": "patterntransform",
-                        "value": ""
-                      },
-                      {
-                        "name": "patternunits",
-                        "value": ""
-                      },
-                      {
-                        "name": "pointsatx",
-                        "value": ""
-                      },
-                      {
-                        "name": "pointsaty",
-                        "value": ""
-                      },
-                      {
-                        "name": "pointsatz",
-                        "value": ""
-                      },
-                      {
-                        "name": "preservealpha",
-                        "value": ""
-                      },
-                      {
-                        "name": "preserveaspectratio",
-                        "value": ""
-                      },
-                      {
-                        "name": "primitiveunits",
-                        "value": ""
-                      },
-                      {
-                        "name": "refx",
-                        "value": ""
-                      },
-                      {
-                        "name": "refy",
-                        "value": ""
-                      },
-                      {
-                        "name": "repeatcount",
-                        "value": ""
-                      },
-                      {
-                        "name": "repeatdur",
-                        "value": ""
-                      },
-                      {
-                        "name": "requiredextensions",
-                        "value": ""
-                      },
-                      {
-                        "name": "requiredfeatures",
-                        "value": ""
-                      },
-                      {
-                        "name": "specularconstant",
-                        "value": ""
-                      },
-                      {
-                        "name": "specularexponent",
-                        "value": ""
-                      },
-                      {
-                        "name": "spreadmethod",
-                        "value": ""
-                      },
-                      {
-                        "name": "startoffset",
-                        "value": ""
-                      },
-                      {
-                        "name": "stddeviation",
-                        "value": ""
-                      },
-                      {
-                        "name": "stitchtiles",
-                        "value": ""
-                      },
-                      {
-                        "name": "surfacescale",
-                        "value": ""
-                      },
-                      {
-                        "name": "systemlanguage",
-                        "value": ""
-                      },
-                      {
-                        "name": "tablevalues",
-                        "value": ""
-                      },
-                      {
-                        "name": "targetx",
-                        "value": ""
-                      },
-                      {
-                        "name": "targety",
-                        "value": ""
-                      },
-                      {
-                        "name": "textlength",
-                        "value": ""
-                      },
-                      {
-                        "name": "viewbox",
-                        "value": ""
-                      },
-                      {
-                        "name": "viewtarget",
-                        "value": ""
-                      },
-                      {
-                        "name": "xchannelselector",
-                        "value": ""
-                      },
-                      {
-                        "name": "ychannelselector",
-                        "value": ""
-                      },
-                      {
-                        "name": "zoomandpan",
-                        "value": ""
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><math attributename=\"\" attributetype=\"\" basefrequency=\"\" baseprofile=\"\" calcmode=\"\" clippathunits=\"\" diffuseconstant=\"\" edgemode=\"\" filterunits=\"\" glyphref=\"\" gradienttransform=\"\" gradientunits=\"\" kernelmatrix=\"\" kernelunitlength=\"\" keypoints=\"\" keysplines=\"\" keytimes=\"\" lengthadjust=\"\" limitingconeangle=\"\" markerheight=\"\" markerunits=\"\" markerwidth=\"\" maskcontentunits=\"\" maskunits=\"\" numoctaves=\"\" pathlength=\"\" patterncontentunits=\"\" patterntransform=\"\" patternunits=\"\" pointsatx=\"\" pointsaty=\"\" pointsatz=\"\" preservealpha=\"\" preserveaspectratio=\"\" primitiveunits=\"\" refx=\"\" refy=\"\" repeatcount=\"\" repeatdur=\"\" requiredextensions=\"\" requiredfeatures=\"\" specularconstant=\"\" specularexponent=\"\" spreadmethod=\"\" startoffset=\"\" stddeviation=\"\" stitchtiles=\"\" surfacescale=\"\" systemlanguage=\"\" tablevalues=\"\" targetx=\"\" targety=\"\" textlength=\"\" viewbox=\"\" viewtarget=\"\" xchannelselector=\"\" ychannelselector=\"\" zoomandpan=\"\"></math></body></html>",
-        "noQuirksBodyHtml": "<math attributename=\"\" attributetype=\"\" basefrequency=\"\" baseprofile=\"\" calcmode=\"\" clippathunits=\"\" diffuseconstant=\"\" edgemode=\"\" filterunits=\"\" glyphref=\"\" gradienttransform=\"\" gradientunits=\"\" kernelmatrix=\"\" kernelunitlength=\"\" keypoints=\"\" keysplines=\"\" keytimes=\"\" lengthadjust=\"\" limitingconeangle=\"\" markerheight=\"\" markerunits=\"\" markerwidth=\"\" maskcontentunits=\"\" maskunits=\"\" numoctaves=\"\" pathlength=\"\" patterncontentunits=\"\" patterntransform=\"\" patternunits=\"\" pointsatx=\"\" pointsaty=\"\" pointsatz=\"\" preservealpha=\"\" preserveaspectratio=\"\" primitiveunits=\"\" refx=\"\" refy=\"\" repeatcount=\"\" repeatdur=\"\" requiredextensions=\"\" requiredfeatures=\"\" specularconstant=\"\" specularexponent=\"\" spreadmethod=\"\" startoffset=\"\" stddeviation=\"\" stitchtiles=\"\" surfacescale=\"\" systemlanguage=\"\" tablevalues=\"\" targetx=\"\" targety=\"\" textlength=\"\" viewbox=\"\" viewtarget=\"\" xchannelselector=\"\" ychannelselector=\"\" zoomandpan=\"\"></math>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><body><svg contentScriptType='' contentStyleType='' externalResourcesRequired='' filterRes=''></svg>",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "svg svg": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "svg",
-                    "ns": "http://www.w3.org/2000/svg",
-                    "attrs": [
-                      {
-                        "name": "contentscripttype",
-                        "value": ""
-                      },
-                      {
-                        "name": "contentstyletype",
-                        "value": ""
-                      },
-                      {
-                        "name": "externalresourcesrequired",
-                        "value": ""
-                      },
-                      {
-                        "name": "filterres",
-                        "value": ""
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><svg contentscripttype=\"\" contentstyletype=\"\" externalresourcesrequired=\"\" filterres=\"\"></svg></body></html>",
-        "noQuirksBodyHtml": "<svg contentScriptType=\"\" contentStyleType=\"\" externalResourcesRequired=\"\" filterres=\"\"></svg>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><body><svg CONTENTSCRIPTTYPE='' CONTENTSTYLETYPE='' EXTERNALRESOURCESREQUIRED='' FILTERRES=''></svg>",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "svg svg": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "svg",
-                    "ns": "http://www.w3.org/2000/svg",
-                    "attrs": [
-                      {
-                        "name": "contentscripttype",
-                        "value": ""
-                      },
-                      {
-                        "name": "contentstyletype",
-                        "value": ""
-                      },
-                      {
-                        "name": "externalresourcesrequired",
-                        "value": ""
-                      },
-                      {
-                        "name": "filterres",
-                        "value": ""
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><svg contentscripttype=\"\" contentstyletype=\"\" externalresourcesrequired=\"\" filterres=\"\"></svg></body></html>",
-        "noQuirksBodyHtml": "<svg contentScriptType=\"\" contentStyleType=\"\" externalResourcesRequired=\"\" filterres=\"\"></svg>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><body><svg contentscripttype='' contentstyletype='' externalresourcesrequired='' filterres=''></svg>",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "svg svg": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "svg",
-                    "ns": "http://www.w3.org/2000/svg",
-                    "attrs": [
-                      {
-                        "name": "contentscripttype",
-                        "value": ""
-                      },
-                      {
-                        "name": "contentstyletype",
-                        "value": ""
-                      },
-                      {
-                        "name": "externalresourcesrequired",
-                        "value": ""
-                      },
-                      {
-                        "name": "filterres",
-                        "value": ""
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><svg contentscripttype=\"\" contentstyletype=\"\" externalresourcesrequired=\"\" filterres=\"\"></svg></body></html>",
-        "noQuirksBodyHtml": "<svg contentScriptType=\"\" contentStyleType=\"\" externalResourcesRequired=\"\" filterres=\"\"></svg>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><body><math contentScriptType='' contentStyleType='' externalResourcesRequired='' filterRes=''></math>",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "math math": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "math",
-                    "ns": "http://www.w3.org/1998/Math/MathML",
-                    "attrs": [
-                      {
-                        "name": "contentscripttype",
-                        "value": ""
-                      },
-                      {
-                        "name": "contentstyletype",
-                        "value": ""
-                      },
-                      {
-                        "name": "externalresourcesrequired",
-                        "value": ""
-                      },
-                      {
-                        "name": "filterres",
-                        "value": ""
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><math contentscripttype=\"\" contentstyletype=\"\" externalresourcesrequired=\"\" filterres=\"\"></math></body></html>",
-        "noQuirksBodyHtml": "<math contentscripttype=\"\" contentstyletype=\"\" externalresourcesrequired=\"\" filterres=\"\"></math>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><body><svg><altGlyph /><altGlyphDef /><altGlyphItem /><animateColor /><animateMotion /><animateTransform /><clipPath /><feBlend /><feColorMatrix /><feComponentTransfer /><feComposite /><feConvolveMatrix /><feDiffuseLighting /><feDisplacementMap /><feDistantLight /><feFlood /><feFuncA /><feFuncB /><feFuncG /><feFuncR /><feGaussianBlur /><feImage /><feMerge /><feMergeNode /><feMorphology /><feOffset /><fePointLight /><feSpecularLighting /><feSpotLight /><feTile /><feTurbulence /><foreignObject /><glyphRef /><linearGradient /><radialGradient /><textPath /></svg>",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "svg svg": true,
-            "svg altGlyph": true,
-            "svg altGlyphDef": true,
-            "svg altGlyphItem": true,
-            "svg animateColor": true,
-            "svg animateMotion": true,
-            "svg animateTransform": true,
-            "svg clipPath": true,
-            "svg feBlend": true,
-            "svg feColorMatrix": true,
-            "svg feComponentTransfer": true,
-            "svg feComposite": true,
-            "svg feConvolveMatrix": true,
-            "svg feDiffuseLighting": true,
-            "svg feDisplacementMap": true,
-            "svg feDistantLight": true,
-            "svg feFlood": true,
-            "svg feFuncA": true,
-            "svg feFuncB": true,
-            "svg feFuncG": true,
-            "svg feFuncR": true,
-            "svg feGaussianBlur": true,
-            "svg feImage": true,
-            "svg feMerge": true,
-            "svg feMergeNode": true,
-            "svg feMorphology": true,
-            "svg feOffset": true,
-            "svg fePointLight": true,
-            "svg feSpecularLighting": true,
-            "svg feSpotLight": true,
-            "svg feTile": true,
-            "svg feTurbulence": true,
-            "svg foreignObject": true,
-            "svg glyphRef": true,
-            "svg linearGradient": true,
-            "svg radialGradient": true,
-            "svg textPath": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "svg",
-                    "ns": "http://www.w3.org/2000/svg",
-                    "children": [
-                      {
-                        "tag": "altGlyph",
-                        "ns": "http://www.w3.org/2000/svg"
-                      },
-                      {
-                        "tag": "altGlyphDef",
-                        "ns": "http://www.w3.org/2000/svg"
-                      },
-                      {
-                        "tag": "altGlyphItem",
-                        "ns": "http://www.w3.org/2000/svg"
-                      },
-                      {
-                        "tag": "animateColor",
-                        "ns": "http://www.w3.org/2000/svg"
-                      },
-                      {
-                        "tag": "animateMotion",
-                        "ns": "http://www.w3.org/2000/svg"
-                      },
-                      {
-                        "tag": "animateTransform",
-                        "ns": "http://www.w3.org/2000/svg"
-                      },
-                      {
-                        "tag": "clipPath",
-                        "ns": "http://www.w3.org/2000/svg"
-                      },
-                      {
-                        "tag": "feBlend",
-                        "ns": "http://www.w3.org/2000/svg"
-                      },
-                      {
-                        "tag": "feColorMatrix",
-                        "ns": "http://www.w3.org/2000/svg"
-                      },
-                      {
-                        "tag": "feComponentTransfer",
-                        "ns": "http://www.w3.org/2000/svg"
-                      },
-                      {
-                        "tag": "feComposite",
-                        "ns": "http://www.w3.org/2000/svg"
-                      },
-                      {
-                        "tag": "feConvolveMatrix",
-                        "ns": "http://www.w3.org/2000/svg"
-                      },
-                      {
-                        "tag": "feDiffuseLighting",
-                        "ns": "http://www.w3.org/2000/svg"
-                      },
-                      {
-                        "tag": "feDisplacementMap",
-                        "ns": "http://www.w3.org/2000/svg"
-                      },
-                      {
-                        "tag": "feDistantLight",
-                        "ns": "http://www.w3.org/2000/svg"
-                      },
-                      {
-                        "tag": "feFlood",
-                        "ns": "http://www.w3.org/2000/svg"
-                      },
-                      {
-                        "tag": "feFuncA",
-                        "ns": "http://www.w3.org/2000/svg"
-                      },
-                      {
-                        "tag": "feFuncB",
-                        "ns": "http://www.w3.org/2000/svg"
-                      },
-                      {
-                        "tag": "feFuncG",
-                        "ns": "http://www.w3.org/2000/svg"
-                      },
-                      {
-                        "tag": "feFuncR",
-                        "ns": "http://www.w3.org/2000/svg"
-                      },
-                      {
-                        "tag": "feGaussianBlur",
-                        "ns": "http://www.w3.org/2000/svg"
-                      },
-                      {
-                        "tag": "feImage",
-                        "ns": "http://www.w3.org/2000/svg"
-                      },
-                      {
-                        "tag": "feMerge",
-                        "ns": "http://www.w3.org/2000/svg"
-                      },
-                      {
-                        "tag": "feMergeNode",
-                        "ns": "http://www.w3.org/2000/svg"
-                      },
-                      {
-                        "tag": "feMorphology",
-                        "ns": "http://www.w3.org/2000/svg"
-                      },
-                      {
-                        "tag": "feOffset",
-                        "ns": "http://www.w3.org/2000/svg"
-                      },
-                      {
-                        "tag": "fePointLight",
-                        "ns": "http://www.w3.org/2000/svg"
-                      },
-                      {
-                        "tag": "feSpecularLighting",
-                        "ns": "http://www.w3.org/2000/svg"
-                      },
-                      {
-                        "tag": "feSpotLight",
-                        "ns": "http://www.w3.org/2000/svg"
-                      },
-                      {
-                        "tag": "feTile",
-                        "ns": "http://www.w3.org/2000/svg"
-                      },
-                      {
-                        "tag": "feTurbulence",
-                        "ns": "http://www.w3.org/2000/svg"
-                      },
-                      {
-                        "tag": "foreignObject",
-                        "ns": "http://www.w3.org/2000/svg"
-                      },
-                      {
-                        "tag": "glyphRef",
-                        "ns": "http://www.w3.org/2000/svg"
-                      },
-                      {
-                        "tag": "linearGradient",
-                        "ns": "http://www.w3.org/2000/svg"
-                      },
-                      {
-                        "tag": "radialGradient",
-                        "ns": "http://www.w3.org/2000/svg"
-                      },
-                      {
-                        "tag": "textPath",
-                        "ns": "http://www.w3.org/2000/svg"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><svg><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></svg></body></html>",
-        "noQuirksBodyHtml": "<svg><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></svg>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><body><svg><altglyph /><altglyphdef /><altglyphitem /><animatecolor /><animatemotion /><animatetransform /><clippath /><feblend /><fecolormatrix /><fecomponenttransfer /><fecomposite /><feconvolvematrix /><fediffuselighting /><fedisplacementmap /><fedistantlight /><feflood /><fefunca /><fefuncb /><fefuncg /><fefuncr /><fegaussianblur /><feimage /><femerge /><femergenode /><femorphology /><feoffset /><fepointlight /><fespecularlighting /><fespotlight /><fetile /><feturbulence /><foreignobject /><glyphref /><lineargradient /><radialgradient /><textpath /></svg>",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "svg svg": true,
-            "svg altGlyph": true,
-            "svg altGlyphDef": true,
-            "svg altGlyphItem": true,
-            "svg animateColor": true,
-            "svg animateMotion": true,
-            "svg animateTransform": true,
-            "svg clipPath": true,
-            "svg feBlend": true,
-            "svg feColorMatrix": true,
-            "svg feComponentTransfer": true,
-            "svg feComposite": true,
-            "svg feConvolveMatrix": true,
-            "svg feDiffuseLighting": true,
-            "svg feDisplacementMap": true,
-            "svg feDistantLight": true,
-            "svg feFlood": true,
-            "svg feFuncA": true,
-            "svg feFuncB": true,
-            "svg feFuncG": true,
-            "svg feFuncR": true,
-            "svg feGaussianBlur": true,
-            "svg feImage": true,
-            "svg feMerge": true,
-            "svg feMergeNode": true,
-            "svg feMorphology": true,
-            "svg feOffset": true,
-            "svg fePointLight": true,
-            "svg feSpecularLighting": true,
-            "svg feSpotLight": true,
-            "svg feTile": true,
-            "svg feTurbulence": true,
-            "svg foreignObject": true,
-            "svg glyphRef": true,
-            "svg linearGradient": true,
-            "svg radialGradient": true,
-            "svg textPath": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "svg",
-                    "ns": "http://www.w3.org/2000/svg",
-                    "children": [
-                      {
-                        "tag": "altGlyph",
-                        "ns": "http://www.w3.org/2000/svg"
-                      },
-                      {
-                        "tag": "altGlyphDef",
-                        "ns": "http://www.w3.org/2000/svg"
-                      },
-                      {
-                        "tag": "altGlyphItem",
-                        "ns": "http://www.w3.org/2000/svg"
-                      },
-                      {
-                        "tag": "animateColor",
-                        "ns": "http://www.w3.org/2000/svg"
-                      },
-                      {
-                        "tag": "animateMotion",
-                        "ns": "http://www.w3.org/2000/svg"
-                      },
-                      {
-                        "tag": "animateTransform",
-                        "ns": "http://www.w3.org/2000/svg"
-                      },
-                      {
-                        "tag": "clipPath",
-                        "ns": "http://www.w3.org/2000/svg"
-                      },
-                      {
-                        "tag": "feBlend",
-                        "ns": "http://www.w3.org/2000/svg"
-                      },
-                      {
-                        "tag": "feColorMatrix",
-                        "ns": "http://www.w3.org/2000/svg"
-                      },
-                      {
-                        "tag": "feComponentTransfer",
-                        "ns": "http://www.w3.org/2000/svg"
-                      },
-                      {
-                        "tag": "feComposite",
-                        "ns": "http://www.w3.org/2000/svg"
-                      },
-                      {
-                        "tag": "feConvolveMatrix",
-                        "ns": "http://www.w3.org/2000/svg"
-                      },
-                      {
-                        "tag": "feDiffuseLighting",
-                        "ns": "http://www.w3.org/2000/svg"
-                      },
-                      {
-                        "tag": "feDisplacementMap",
-                        "ns": "http://www.w3.org/2000/svg"
-                      },
-                      {
-                        "tag": "feDistantLight",
-                        "ns": "http://www.w3.org/2000/svg"
-                      },
-                      {
-                        "tag": "feFlood",
-                        "ns": "http://www.w3.org/2000/svg"
-                      },
-                      {
-                        "tag": "feFuncA",
-                        "ns": "http://www.w3.org/2000/svg"
-                      },
-                      {
-                        "tag": "feFuncB",
-                        "ns": "http://www.w3.org/2000/svg"
-                      },
-                      {
-                        "tag": "feFuncG",
-                        "ns": "http://www.w3.org/2000/svg"
-                      },
-                      {
-                        "tag": "feFuncR",
-                        "ns": "http://www.w3.org/2000/svg"
-                      },
-                      {
-                        "tag": "feGaussianBlur",
-                        "ns": "http://www.w3.org/2000/svg"
-                      },
-                      {
-                        "tag": "feImage",
-                        "ns": "http://www.w3.org/2000/svg"
-                      },
-                      {
-                        "tag": "feMerge",
-                        "ns": "http://www.w3.org/2000/svg"
-                      },
-                      {
-                        "tag": "feMergeNode",
-                        "ns": "http://www.w3.org/2000/svg"
-                      },
-                      {
-                        "tag": "feMorphology",
-                        "ns": "http://www.w3.org/2000/svg"
-                      },
-                      {
-                        "tag": "feOffset",
-                        "ns": "http://www.w3.org/2000/svg"
-                      },
-                      {
-                        "tag": "fePointLight",
-                        "ns": "http://www.w3.org/2000/svg"
-                      },
-                      {
-                        "tag": "feSpecularLighting",
-                        "ns": "http://www.w3.org/2000/svg"
-                      },
-                      {
-                        "tag": "feSpotLight",
-                        "ns": "http://www.w3.org/2000/svg"
-                      },
-                      {
-                        "tag": "feTile",
-                        "ns": "http://www.w3.org/2000/svg"
-                      },
-                      {
-                        "tag": "feTurbulence",
-                        "ns": "http://www.w3.org/2000/svg"
-                      },
-                      {
-                        "tag": "foreignObject",
-                        "ns": "http://www.w3.org/2000/svg"
-                      },
-                      {
-                        "tag": "glyphRef",
-                        "ns": "http://www.w3.org/2000/svg"
-                      },
-                      {
-                        "tag": "linearGradient",
-                        "ns": "http://www.w3.org/2000/svg"
-                      },
-                      {
-                        "tag": "radialGradient",
-                        "ns": "http://www.w3.org/2000/svg"
-                      },
-                      {
-                        "tag": "textPath",
-                        "ns": "http://www.w3.org/2000/svg"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><svg><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></svg></body></html>",
-        "noQuirksBodyHtml": "<svg><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></svg>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><BODY><SVG><ALTGLYPH /><ALTGLYPHDEF /><ALTGLYPHITEM /><ANIMATECOLOR /><ANIMATEMOTION /><ANIMATETRANSFORM /><CLIPPATH /><FEBLEND /><FECOLORMATRIX /><FECOMPONENTTRANSFER /><FECOMPOSITE /><FECONVOLVEMATRIX /><FEDIFFUSELIGHTING /><FEDISPLACEMENTMAP /><FEDISTANTLIGHT /><FEFLOOD /><FEFUNCA /><FEFUNCB /><FEFUNCG /><FEFUNCR /><FEGAUSSIANBLUR /><FEIMAGE /><FEMERGE /><FEMERGENODE /><FEMORPHOLOGY /><FEOFFSET /><FEPOINTLIGHT /><FESPECULARLIGHTING /><FESPOTLIGHT /><FETILE /><FETURBULENCE /><FOREIGNOBJECT /><GLYPHREF /><LINEARGRADIENT /><RADIALGRADIENT /><TEXTPATH /></SVG>",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "svg svg": true,
-            "svg altGlyph": true,
-            "svg altGlyphDef": true,
-            "svg altGlyphItem": true,
-            "svg animateColor": true,
-            "svg animateMotion": true,
-            "svg animateTransform": true,
-            "svg clipPath": true,
-            "svg feBlend": true,
-            "svg feColorMatrix": true,
-            "svg feComponentTransfer": true,
-            "svg feComposite": true,
-            "svg feConvolveMatrix": true,
-            "svg feDiffuseLighting": true,
-            "svg feDisplacementMap": true,
-            "svg feDistantLight": true,
-            "svg feFlood": true,
-            "svg feFuncA": true,
-            "svg feFuncB": true,
-            "svg feFuncG": true,
-            "svg feFuncR": true,
-            "svg feGaussianBlur": true,
-            "svg feImage": true,
-            "svg feMerge": true,
-            "svg feMergeNode": true,
-            "svg feMorphology": true,
-            "svg feOffset": true,
-            "svg fePointLight": true,
-            "svg feSpecularLighting": true,
-            "svg feSpotLight": true,
-            "svg feTile": true,
-            "svg feTurbulence": true,
-            "svg foreignObject": true,
-            "svg glyphRef": true,
-            "svg linearGradient": true,
-            "svg radialGradient": true,
-            "svg textPath": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "svg",
-                    "ns": "http://www.w3.org/2000/svg",
-                    "children": [
-                      {
-                        "tag": "altGlyph",
-                        "ns": "http://www.w3.org/2000/svg"
-                      },
-                      {
-                        "tag": "altGlyphDef",
-                        "ns": "http://www.w3.org/2000/svg"
-                      },
-                      {
-                        "tag": "altGlyphItem",
-                        "ns": "http://www.w3.org/2000/svg"
-                      },
-                      {
-                        "tag": "animateColor",
-                        "ns": "http://www.w3.org/2000/svg"
-                      },
-                      {
-                        "tag": "animateMotion",
-                        "ns": "http://www.w3.org/2000/svg"
-                      },
-                      {
-                        "tag": "animateTransform",
-                        "ns": "http://www.w3.org/2000/svg"
-                      },
-                      {
-                        "tag": "clipPath",
-                        "ns": "http://www.w3.org/2000/svg"
-                      },
-                      {
-                        "tag": "feBlend",
-                        "ns": "http://www.w3.org/2000/svg"
-                      },
-                      {
-                        "tag": "feColorMatrix",
-                        "ns": "http://www.w3.org/2000/svg"
-                      },
-                      {
-                        "tag": "feComponentTransfer",
-                        "ns": "http://www.w3.org/2000/svg"
-                      },
-                      {
-                        "tag": "feComposite",
-                        "ns": "http://www.w3.org/2000/svg"
-                      },
-                      {
-                        "tag": "feConvolveMatrix",
-                        "ns": "http://www.w3.org/2000/svg"
-                      },
-                      {
-                        "tag": "feDiffuseLighting",
-                        "ns": "http://www.w3.org/2000/svg"
-                      },
-                      {
-                        "tag": "feDisplacementMap",
-                        "ns": "http://www.w3.org/2000/svg"
-                      },
-                      {
-                        "tag": "feDistantLight",
-                        "ns": "http://www.w3.org/2000/svg"
-                      },
-                      {
-                        "tag": "feFlood",
-                        "ns": "http://www.w3.org/2000/svg"
-                      },
-                      {
-                        "tag": "feFuncA",
-                        "ns": "http://www.w3.org/2000/svg"
-                      },
-                      {
-                        "tag": "feFuncB",
-                        "ns": "http://www.w3.org/2000/svg"
-                      },
-                      {
-                        "tag": "feFuncG",
-                        "ns": "http://www.w3.org/2000/svg"
-                      },
-                      {
-                        "tag": "feFuncR",
-                        "ns": "http://www.w3.org/2000/svg"
-                      },
-                      {
-                        "tag": "feGaussianBlur",
-                        "ns": "http://www.w3.org/2000/svg"
-                      },
-                      {
-                        "tag": "feImage",
-                        "ns": "http://www.w3.org/2000/svg"
-                      },
-                      {
-                        "tag": "feMerge",
-                        "ns": "http://www.w3.org/2000/svg"
-                      },
-                      {
-                        "tag": "feMergeNode",
-                        "ns": "http://www.w3.org/2000/svg"
-                      },
-                      {
-                        "tag": "feMorphology",
-                        "ns": "http://www.w3.org/2000/svg"
-                      },
-                      {
-                        "tag": "feOffset",
-                        "ns": "http://www.w3.org/2000/svg"
-                      },
-                      {
-                        "tag": "fePointLight",
-                        "ns": "http://www.w3.org/2000/svg"
-                      },
-                      {
-                        "tag": "feSpecularLighting",
-                        "ns": "http://www.w3.org/2000/svg"
-                      },
-                      {
-                        "tag": "feSpotLight",
-                        "ns": "http://www.w3.org/2000/svg"
-                      },
-                      {
-                        "tag": "feTile",
-                        "ns": "http://www.w3.org/2000/svg"
-                      },
-                      {
-                        "tag": "feTurbulence",
-                        "ns": "http://www.w3.org/2000/svg"
-                      },
-                      {
-                        "tag": "foreignObject",
-                        "ns": "http://www.w3.org/2000/svg"
-                      },
-                      {
-                        "tag": "glyphRef",
-                        "ns": "http://www.w3.org/2000/svg"
-                      },
-                      {
-                        "tag": "linearGradient",
-                        "ns": "http://www.w3.org/2000/svg"
-                      },
-                      {
-                        "tag": "radialGradient",
-                        "ns": "http://www.w3.org/2000/svg"
-                      },
-                      {
-                        "tag": "textPath",
-                        "ns": "http://www.w3.org/2000/svg"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><svg><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></svg></body></html>",
-        "noQuirksBodyHtml": "<svg><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></svg>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><body><math><altGlyph /><altGlyphDef /><altGlyphItem /><animateColor /><animateMotion /><animateTransform /><clipPath /><feBlend /><feColorMatrix /><feComponentTransfer /><feComposite /><feConvolveMatrix /><feDiffuseLighting /><feDisplacementMap /><feDistantLight /><feFlood /><feFuncA /><feFuncB /><feFuncG /><feFuncR /><feGaussianBlur /><feImage /><feMerge /><feMergeNode /><feMorphology /><feOffset /><fePointLight /><feSpecularLighting /><feSpotLight /><feTile /><feTurbulence /><foreignObject /><glyphRef /><linearGradient /><radialGradient /><textPath /></math>",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "math math": true,
-            "math altglyph": true,
-            "math altglyphdef": true,
-            "math altglyphitem": true,
-            "math animatecolor": true,
-            "math animatemotion": true,
-            "math animatetransform": true,
-            "math clippath": true,
-            "math feblend": true,
-            "math fecolormatrix": true,
-            "math fecomponenttransfer": true,
-            "math fecomposite": true,
-            "math feconvolvematrix": true,
-            "math fediffuselighting": true,
-            "math fedisplacementmap": true,
-            "math fedistantlight": true,
-            "math feflood": true,
-            "math fefunca": true,
-            "math fefuncb": true,
-            "math fefuncg": true,
-            "math fefuncr": true,
-            "math fegaussianblur": true,
-            "math feimage": true,
-            "math femerge": true,
-            "math femergenode": true,
-            "math femorphology": true,
-            "math feoffset": true,
-            "math fepointlight": true,
-            "math fespecularlighting": true,
-            "math fespotlight": true,
-            "math fetile": true,
-            "math feturbulence": true,
-            "math foreignobject": true,
-            "math glyphref": true,
-            "math lineargradient": true,
-            "math radialgradient": true,
-            "math textpath": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "math",
-                    "ns": "http://www.w3.org/1998/Math/MathML",
-                    "children": [
-                      {
-                        "tag": "altglyph",
-                        "ns": "http://www.w3.org/1998/Math/MathML"
-                      },
-                      {
-                        "tag": "altglyphdef",
-                        "ns": "http://www.w3.org/1998/Math/MathML"
-                      },
-                      {
-                        "tag": "altglyphitem",
-                        "ns": "http://www.w3.org/1998/Math/MathML"
-                      },
-                      {
-                        "tag": "animatecolor",
-                        "ns": "http://www.w3.org/1998/Math/MathML"
-                      },
-                      {
-                        "tag": "animatemotion",
-                        "ns": "http://www.w3.org/1998/Math/MathML"
-                      },
-                      {
-                        "tag": "animatetransform",
-                        "ns": "http://www.w3.org/1998/Math/MathML"
-                      },
-                      {
-                        "tag": "clippath",
-                        "ns": "http://www.w3.org/1998/Math/MathML"
-                      },
-                      {
-                        "tag": "feblend",
-                        "ns": "http://www.w3.org/1998/Math/MathML"
-                      },
-                      {
-                        "tag": "fecolormatrix",
-                        "ns": "http://www.w3.org/1998/Math/MathML"
-                      },
-                      {
-                        "tag": "fecomponenttransfer",
-                        "ns": "http://www.w3.org/1998/Math/MathML"
-                      },
-                      {
-                        "tag": "fecomposite",
-                        "ns": "http://www.w3.org/1998/Math/MathML"
-                      },
-                      {
-                        "tag": "feconvolvematrix",
-                        "ns": "http://www.w3.org/1998/Math/MathML"
-                      },
-                      {
-                        "tag": "fediffuselighting",
-                        "ns": "http://www.w3.org/1998/Math/MathML"
-                      },
-                      {
-                        "tag": "fedisplacementmap",
-                        "ns": "http://www.w3.org/1998/Math/MathML"
-                      },
-                      {
-                        "tag": "fedistantlight",
-                        "ns": "http://www.w3.org/1998/Math/MathML"
-                      },
-                      {
-                        "tag": "feflood",
-                        "ns": "http://www.w3.org/1998/Math/MathML"
-                      },
-                      {
-                        "tag": "fefunca",
-                        "ns": "http://www.w3.org/1998/Math/MathML"
-                      },
-                      {
-                        "tag": "fefuncb",
-                        "ns": "http://www.w3.org/1998/Math/MathML"
-                      },
-                      {
-                        "tag": "fefuncg",
-                        "ns": "http://www.w3.org/1998/Math/MathML"
-                      },
-                      {
-                        "tag": "fefuncr",
-                        "ns": "http://www.w3.org/1998/Math/MathML"
-                      },
-                      {
-                        "tag": "fegaussianblur",
-                        "ns": "http://www.w3.org/1998/Math/MathML"
-                      },
-                      {
-                        "tag": "feimage",
-                        "ns": "http://www.w3.org/1998/Math/MathML"
-                      },
-                      {
-                        "tag": "femerge",
-                        "ns": "http://www.w3.org/1998/Math/MathML"
-                      },
-                      {
-                        "tag": "femergenode",
-                        "ns": "http://www.w3.org/1998/Math/MathML"
-                      },
-                      {
-                        "tag": "femorphology",
-                        "ns": "http://www.w3.org/1998/Math/MathML"
-                      },
-                      {
-                        "tag": "feoffset",
-                        "ns": "http://www.w3.org/1998/Math/MathML"
-                      },
-                      {
-                        "tag": "fepointlight",
-                        "ns": "http://www.w3.org/1998/Math/MathML"
-                      },
-                      {
-                        "tag": "fespecularlighting",
-                        "ns": "http://www.w3.org/1998/Math/MathML"
-                      },
-                      {
-                        "tag": "fespotlight",
-                        "ns": "http://www.w3.org/1998/Math/MathML"
-                      },
-                      {
-                        "tag": "fetile",
-                        "ns": "http://www.w3.org/1998/Math/MathML"
-                      },
-                      {
-                        "tag": "feturbulence",
-                        "ns": "http://www.w3.org/1998/Math/MathML"
-                      },
-                      {
-                        "tag": "foreignobject",
-                        "ns": "http://www.w3.org/1998/Math/MathML"
-                      },
-                      {
-                        "tag": "glyphref",
-                        "ns": "http://www.w3.org/1998/Math/MathML"
-                      },
-                      {
-                        "tag": "lineargradient",
-                        "ns": "http://www.w3.org/1998/Math/MathML"
-                      },
-                      {
-                        "tag": "radialgradient",
-                        "ns": "http://www.w3.org/1998/Math/MathML"
-                      },
-                      {
-                        "tag": "textpath",
-                        "ns": "http://www.w3.org/1998/Math/MathML"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><math><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></math></body></html>",
-        "noQuirksBodyHtml": "<math><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></math>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><body><svg><solidColor /></svg>",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "svg svg": true,
-            "svg solidcolor": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "svg",
-                    "ns": "http://www.w3.org/2000/svg",
-                    "children": [
-                      {
-                        "tag": "solidcolor",
-                        "ns": "http://www.w3.org/2000/svg"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><svg><solidcolor></solidcolor></svg></body></html>",
-        "noQuirksBodyHtml": "<svg><solidcolor></solidcolor></svg>"
-      }
-    }
-  ],
-  "tests12.dat": [
-    {
-      "data": "<!DOCTYPE html><body><p>foo<math><mtext><i>baz</i></mtext><annotation-xml><svg><desc><b>eggs</b></desc><g><foreignObject><P>spam<TABLE><tr><td><img></td></table></foreignObject></g><g>quux</g></svg></annotation-xml></math>bar",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "p": true,
-            "math math": true,
-            "math mtext": true,
-            "i": true,
-            "math annotation-xml": true,
-            "svg svg": true,
-            "svg desc": true,
-            "b": true,
-            "svg g": true,
-            "svg foreignObject": true,
-            "table": true,
-            "tbody": true,
-            "tr": true,
-            "td": true,
-            "img": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "p",
-                    "children": [
-                      {
-                        "text": "foo"
-                      },
-                      {
-                        "tag": "math",
-                        "ns": "http://www.w3.org/1998/Math/MathML",
-                        "children": [
-                          {
-                            "tag": "mtext",
-                            "ns": "http://www.w3.org/1998/Math/MathML",
-                            "children": [
-                              {
-                                "tag": "i",
-                                "children": [
-                                  {
-                                    "text": "baz"
-                                  }
-                                ]
-                              }
-                            ]
-                          },
-                          {
-                            "tag": "annotation-xml",
-                            "ns": "http://www.w3.org/1998/Math/MathML",
-                            "children": [
-                              {
-                                "tag": "svg",
-                                "ns": "http://www.w3.org/2000/svg",
-                                "children": [
-                                  {
-                                    "tag": "desc",
-                                    "ns": "http://www.w3.org/2000/svg",
-                                    "children": [
-                                      {
-                                        "tag": "b",
-                                        "children": [
-                                          {
-                                            "text": "eggs"
-                                          }
-                                        ]
-                                      }
-                                    ]
-                                  },
-                                  {
-                                    "tag": "g",
-                                    "ns": "http://www.w3.org/2000/svg",
-                                    "children": [
-                                      {
-                                        "tag": "foreignObject",
-                                        "ns": "http://www.w3.org/2000/svg",
-                                        "children": [
-                                          {
-                                            "tag": "p",
-                                            "children": [
-                                              {
-                                                "text": "spam"
-                                              }
-                                            ]
-                                          },
-                                          {
-                                            "tag": "table",
-                                            "children": [
-                                              {
-                                                "tag": "tbody",
-                                                "children": [
-                                                  {
-                                                    "tag": "tr",
-                                                    "children": [
-                                                      {
-                                                        "tag": "td",
-                                                        "children": [
-                                                          {
-                                                            "tag": "img"
-                                                          }
-                                                        ]
-                                                      }
-                                                    ]
-                                                  }
-                                                ]
-                                              }
-                                            ]
-                                          }
-                                        ]
-                                      }
-                                    ]
-                                  },
-                                  {
-                                    "tag": "g",
-                                    "ns": "http://www.w3.org/2000/svg",
-                                    "children": [
-                                      {
-                                        "text": "quux"
-                                      }
-                                    ]
-                                  }
-                                ]
-                              }
-                            ]
-                          }
-                        ]
-                      },
-                      {
-                        "text": "bar"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><p>foo<math><mtext><i>baz</i></mtext><annotation-xml><svg><desc><b>eggs</b></desc><g><foreignObject><p>spam</p><table><tbody><tr><td><img></td></tr></tbody></table></foreignObject></g><g>quux</g></svg></annotation-xml></math>bar</p></body></html>",
-        "noQuirksBodyHtml": "<p>foo<math><mtext><i>baz</i></mtext><annotation-xml><svg><desc><b>eggs</b></desc><g><foreignObject><p>spam</p><table><tbody><tr><td><img></td></tr></tbody></table></foreignObject></g><g>quux</g></svg></annotation-xml></math>bar</p>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><body>foo<math><mtext><i>baz</i></mtext><annotation-xml><svg><desc><b>eggs</b></desc><g><foreignObject><P>spam<TABLE><tr><td><img></td></table></foreignObject></g><g>quux</g></svg></annotation-xml></math>bar",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "math math": true,
-            "math mtext": true,
-            "i": true,
-            "math annotation-xml": true,
-            "svg svg": true,
-            "svg desc": true,
-            "b": true,
-            "svg g": true,
-            "svg foreignObject": true,
-            "p": true,
-            "table": true,
-            "tbody": true,
-            "tr": true,
-            "td": true,
-            "img": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "foo"
-                  },
-                  {
-                    "tag": "math",
-                    "ns": "http://www.w3.org/1998/Math/MathML",
-                    "children": [
-                      {
-                        "tag": "mtext",
-                        "ns": "http://www.w3.org/1998/Math/MathML",
-                        "children": [
-                          {
-                            "tag": "i",
-                            "children": [
-                              {
-                                "text": "baz"
-                              }
-                            ]
-                          }
-                        ]
-                      },
-                      {
-                        "tag": "annotation-xml",
-                        "ns": "http://www.w3.org/1998/Math/MathML",
-                        "children": [
-                          {
-                            "tag": "svg",
-                            "ns": "http://www.w3.org/2000/svg",
-                            "children": [
-                              {
-                                "tag": "desc",
-                                "ns": "http://www.w3.org/2000/svg",
-                                "children": [
-                                  {
-                                    "tag": "b",
-                                    "children": [
-                                      {
-                                        "text": "eggs"
-                                      }
-                                    ]
-                                  }
-                                ]
-                              },
-                              {
-                                "tag": "g",
-                                "ns": "http://www.w3.org/2000/svg",
-                                "children": [
-                                  {
-                                    "tag": "foreignObject",
-                                    "ns": "http://www.w3.org/2000/svg",
-                                    "children": [
-                                      {
-                                        "tag": "p",
-                                        "children": [
-                                          {
-                                            "text": "spam"
-                                          }
-                                        ]
-                                      },
-                                      {
-                                        "tag": "table",
-                                        "children": [
-                                          {
-                                            "tag": "tbody",
-                                            "children": [
-                                              {
-                                                "tag": "tr",
-                                                "children": [
-                                                  {
-                                                    "tag": "td",
-                                                    "children": [
-                                                      {
-                                                        "tag": "img"
-                                                      }
-                                                    ]
-                                                  }
-                                                ]
-                                              }
-                                            ]
-                                          }
-                                        ]
-                                      }
-                                    ]
-                                  }
-                                ]
-                              },
-                              {
-                                "tag": "g",
-                                "ns": "http://www.w3.org/2000/svg",
-                                "children": [
-                                  {
-                                    "text": "quux"
-                                  }
-                                ]
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  },
-                  {
-                    "text": "bar"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body>foo<math><mtext><i>baz</i></mtext><annotation-xml><svg><desc><b>eggs</b></desc><g><foreignObject><p>spam</p><table><tbody><tr><td><img></td></tr></tbody></table></foreignObject></g><g>quux</g></svg></annotation-xml></math>bar</body></html>",
-        "noQuirksBodyHtml": "foo<math><mtext><i>baz</i></mtext><annotation-xml><svg><desc><b>eggs</b></desc><g><foreignObject><p>spam</p><table><tbody><tr><td><img></td></tr></tbody></table></foreignObject></g><g>quux</g></svg></annotation-xml></math>bar"
-      }
-    }
-  ],
-  "tests14.dat": [
-    {
-      "data": "<!DOCTYPE html><html><body><xyz:abc></xyz:abc>",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "xyz:abc": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "xyz:abc"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><xyz:abc></xyz:abc></body></html>",
-        "noQuirksBodyHtml": "<xyz:abc></xyz:abc>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><html><body><xyz:abc></xyz:abc><span></span>",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "xyz:abc": true,
-            "span": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "xyz:abc"
-                  },
-                  {
-                    "tag": "span"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><xyz:abc></xyz:abc><span></span></body></html>",
-        "noQuirksBodyHtml": "<xyz:abc></xyz:abc><span></span>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><html><html abc:def=gh><xyz:abc></xyz:abc>",
-      "errors": [
-        "(1,38): non-html-root"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "xyz:abc": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "attrs": [
-              {
-                "name": "abc:def",
-                "value": "gh"
-              }
-            ],
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "xyz:abc"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html abc:def=\"gh\"><head></head><body><xyz:abc></xyz:abc></body></html>",
-        "noQuirksBodyHtml": "<xyz:abc></xyz:abc>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><html xml:lang=bar><html xml:lang=foo>",
-      "errors": [
-        "(1,53): non-html-root"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "attrs": [
-              {
-                "name": "xml:lang",
-                "value": "bar"
-              }
-            ],
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html xml:lang=\"bar\"><head></head><body></body></html>",
-        "noQuirksBodyHtml": ""
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><html 123=456>",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "attrs": [
-              {
-                "name": "123",
-                "value": "456"
-              }
-            ],
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html 123=\"456\"><head></head><body></body></html>",
-        "noQuirksBodyHtml": ""
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><html 123=456><html 789=012>",
-      "errors": [
-        "(1,43): non-html-root"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "attrs": [
-              {
-                "name": "123",
-                "value": "456"
-              },
-              {
-                "name": "789",
-                "value": "012"
-              }
-            ],
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html 123=\"456\" 789=\"012\"><head></head><body></body></html>",
-        "noQuirksBodyHtml": ""
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><html><body 789=012>",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "attrs": [
-                  {
-                    "name": "789",
-                    "value": "012"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body 789=\"012\"></body></html>",
-        "noQuirksBodyHtml": ""
-      }
-    }
-  ],
-  "tests15.dat": [
-    {
-      "data": "<!DOCTYPE html><p><b><i><u></p> <p>X",
-      "errors": [
-        "(1,31): unexpected-end-tag",
-        "(1,36): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "p": true,
-            "b": true,
-            "i": true,
-            "u": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "p",
-                    "children": [
-                      {
-                        "tag": "b",
-                        "children": [
-                          {
-                            "tag": "i",
-                            "children": [
-                              {
-                                "tag": "u"
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "b",
-                    "children": [
-                      {
-                        "tag": "i",
-                        "children": [
-                          {
-                            "tag": "u",
-                            "children": [
-                              {
-                                "text": " "
-                              },
-                              {
-                                "tag": "p",
-                                "children": [
-                                  {
-                                    "text": "X"
-                                  }
-                                ]
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><p><b><i><u></u></i></b></p><b><i><u> <p>X</p></u></i></b></body></html>",
-        "noQuirksBodyHtml": "<p><b><i><u></u></i></b></p><b><i><u> <p>X</p></u></i></b>"
-      }
-    },
-    {
-      "data": "<p><b><i><u></p>\n<p>X",
-      "errors": [
-        "(1,3): expected-doctype-but-got-start-tag",
-        "(1,16): unexpected-end-tag",
-        "(2,4): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "p": true,
-            "b": true,
-            "i": true,
-            "u": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "p",
-                    "children": [
-                      {
-                        "tag": "b",
-                        "children": [
-                          {
-                            "tag": "i",
-                            "children": [
-                              {
-                                "tag": "u"
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "b",
-                    "children": [
-                      {
-                        "tag": "i",
-                        "children": [
-                          {
-                            "tag": "u",
-                            "children": [
-                              {
-                                "text": "\n"
-                              },
-                              {
-                                "tag": "p",
-                                "children": [
-                                  {
-                                    "text": "X"
-                                  }
-                                ]
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><p><b><i><u></u></i></b></p><b><i><u>\n<p>X</p></u></i></b></body></html>",
-        "noQuirksBodyHtml": "<p><b><i><u></u></i></b></p><b><i><u>\n<p>X</p></u></i></b>"
-      }
-    },
-    {
-      "data": "<!doctype html></html> <head>",
-      "errors": [
-        "(1,29): expected-eof-but-got-start-tag",
-        "(1,29): unexpected-start-tag-ignored"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": " "
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body> </body></html>",
-        "noQuirksBodyHtml": " "
-      }
-    },
-    {
-      "data": "<!doctype html></body><meta>",
-      "errors": [
-        "(1,28): unexpected-start-tag-after-body"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "meta": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "meta"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><meta></body></html>",
-        "noQuirksBodyHtml": "<meta>"
-      }
-    },
-    {
-      "data": "<html></html><!-- foo -->",
-      "errors": [
-        "(1,6): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "comment": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          },
-          {
-            "comment": " foo "
-          }
-        ],
-        "html": "<html><head></head><body></body></html><!-- foo -->",
-        "noQuirksBodyHtml": "<!-- foo -->"
-      }
-    },
-    {
-      "data": "<!doctype html></body><title>X</title>",
-      "errors": [
-        "(1,29): unexpected-start-tag-after-body"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "title": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "title",
-                    "children": [
-                      {
-                        "text": "X"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><title>X</title></body></html>",
-        "noQuirksBodyHtml": "<title>X</title>"
-      }
-    },
-    {
-      "data": "<!doctype html><table> X<meta></table>",
-      "errors": [
-        "(1,23): foster-parenting-character",
-        "(1,24): foster-parenting-character",
-        "(1,30): foster-parenting-start-character"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "meta": true,
-            "table": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": " X"
-                  },
-                  {
-                    "tag": "meta"
-                  },
-                  {
-                    "tag": "table"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body> X<meta><table></table></body></html>",
-        "noQuirksBodyHtml": " X<meta><table></table>"
-      }
-    },
-    {
-      "data": "<!doctype html><table> x</table>",
-      "errors": [
-        "(1,23): foster-parenting-character",
-        "(1,24): foster-parenting-character"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": " x"
-                  },
-                  {
-                    "tag": "table"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body> x<table></table></body></html>",
-        "noQuirksBodyHtml": " x<table></table>"
-      }
-    },
-    {
-      "data": "<!doctype html><table> x </table>",
-      "errors": [
-        "(1,23): foster-parenting-character",
-        "(1,24): foster-parenting-character",
-        "(1,25): foster-parenting-character"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": " x "
-                  },
-                  {
-                    "tag": "table"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body> x <table></table></body></html>",
-        "noQuirksBodyHtml": " x <table></table>"
-      }
-    },
-    {
-      "data": "<!doctype html><table><tr> x</table>",
-      "errors": [
-        "(1,27): foster-parenting-character",
-        "(1,28): foster-parenting-character"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true,
-            "tbody": true,
-            "tr": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": " x"
-                  },
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "tbody",
-                        "children": [
-                          {
-                            "tag": "tr"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body> x<table><tbody><tr></tr></tbody></table></body></html>",
-        "noQuirksBodyHtml": " x<table><tbody><tr></tr></tbody></table>"
-      }
-    },
-    {
-      "data": "<!doctype html><table>X<style> <tr>x </style> </table>",
-      "errors": [
-        "(1,23): foster-parenting-character"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true,
-            "style": true
-          },
-          "doctype": true,
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "X"
-                  },
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "style",
-                        "children": [
-                          {
-                            "text": " <tr>x ",
-                            "no_escape": true
-                          }
-                        ]
-                      },
-                      {
-                        "text": " "
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body>X<table><style> <tr>x </style> </table></body></html>",
-        "noQuirksBodyHtml": "X<table><style> <tr>x </style> </table>"
-      }
-    },
-    {
-      "data": "<!doctype html><div><table><a>foo</a> <tr><td>bar</td> </tr></table></div>",
-      "errors": [
-        "(1,30): foster-parenting-start-tag",
-        "(1,31): foster-parenting-character",
-        "(1,32): foster-parenting-character",
-        "(1,33): foster-parenting-character",
-        "(1,37): foster-parenting-end-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "div": true,
-            "a": true,
-            "table": true,
-            "tbody": true,
-            "tr": true,
-            "td": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "div",
-                    "children": [
-                      {
-                        "tag": "a",
-                        "children": [
-                          {
-                            "text": "foo"
-                          }
-                        ]
-                      },
-                      {
-                        "tag": "table",
-                        "children": [
-                          {
-                            "text": " "
-                          },
-                          {
-                            "tag": "tbody",
-                            "children": [
-                              {
-                                "tag": "tr",
-                                "children": [
-                                  {
-                                    "tag": "td",
-                                    "children": [
-                                      {
-                                        "text": "bar"
-                                      }
-                                    ]
-                                  },
-                                  {
-                                    "text": " "
-                                  }
-                                ]
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><div><a>foo</a><table> <tbody><tr><td>bar</td> </tr></tbody></table></div></body></html>",
-        "noQuirksBodyHtml": "<div><a>foo</a><table> <tbody><tr><td>bar</td> </tr></tbody></table></div>"
-      }
-    },
-    {
-      "data": "<frame></frame></frame><frameset><frame><frameset><frame></frameset><noframes></frameset><noframes>",
-      "errors": [
-        "(1,7): expected-doctype-but-got-start-tag",
-        "(1,7): unexpected-start-tag-ignored",
-        "(1,15): unexpected-end-tag",
-        "(1,23): unexpected-end-tag",
-        "(1,33): unexpected-start-tag",
-        "(1,99): expected-named-closing-tag-but-got-eof",
-        "(1,99): eof-in-frameset"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "frameset": true,
-            "frame": true,
-            "noframes": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "frameset",
-                "children": [
-                  {
-                    "tag": "frame"
-                  },
-                  {
-                    "tag": "frameset",
-                    "children": [
-                      {
-                        "tag": "frame"
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "noframes",
-                    "children": [
-                      {
-                        "text": "</frameset><noframes>",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><frameset><frame><frameset><frame></frameset><noframes></frameset><noframes></noframes></frameset></html>",
-        "noQuirksBodyHtml": "<noframes></frameset><noframes></noframes>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><object></html>",
-      "errors": [
-        "(1,30): expected-body-in-scope",
-        "(1,30): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "object": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "object"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><object></object></body></html>",
-        "noQuirksBodyHtml": "<object></object>"
-      }
-    }
-  ],
-  "tests16.dat": [
-    {
-      "data": "<!doctype html><script>",
-      "errors": [
-        "(1,23): expected-named-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script"
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head><script></script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script></script>"
-      }
-    },
-    {
-      "data": "<!doctype html><script>a",
-      "errors": [
-        "(1,24): expected-named-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "doctype": true,
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "a",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head><script>a</script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script>a</script>"
-      }
-    },
-    {
-      "data": "<!doctype html><script><",
-      "errors": [
-        "(1,24): expected-named-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "doctype": true,
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "<",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head><script><</script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script><</script>"
-      }
-    },
-    {
-      "data": "<!doctype html><script></",
-      "errors": [
-        "(1,25): expected-named-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "doctype": true,
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "</",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head><script></</script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script></</script>"
-      }
-    },
-    {
-      "data": "<!doctype html><script></S",
-      "errors": [
-        "(1,26): expected-named-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "doctype": true,
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "</S",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head><script></S</script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script></S</script>"
-      }
-    },
-    {
-      "data": "<!doctype html><script></SC",
-      "errors": [
-        "(1,27): expected-named-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "doctype": true,
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "</SC",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head><script></SC</script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script></SC</script>"
-      }
-    },
-    {
-      "data": "<!doctype html><script></SCR",
-      "errors": [
-        "(1,28): expected-named-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "doctype": true,
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "</SCR",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head><script></SCR</script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script></SCR</script>"
-      }
-    },
-    {
-      "data": "<!doctype html><script></SCRI",
-      "errors": [
-        "(1,29): expected-named-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "doctype": true,
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "</SCRI",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head><script></SCRI</script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script></SCRI</script>"
-      }
-    },
-    {
-      "data": "<!doctype html><script></SCRIP",
-      "errors": [
-        "(1,30): expected-named-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "doctype": true,
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "</SCRIP",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head><script></SCRIP</script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script></SCRIP</script>"
-      }
-    },
-    {
-      "data": "<!doctype html><script></SCRIPT",
-      "errors": [
-        "(1,31): expected-named-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "doctype": true,
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "</SCRIPT",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head><script></SCRIPT</script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script></SCRIPT</script>"
-      }
-    },
-    {
-      "data": "<!doctype html><script></SCRIPT ",
-      "errors": [
-        "(1,32): expected-attribute-name-but-got-eof",
-        "(1,32): expected-named-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script"
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head><script></script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script></script>"
-      }
-    },
-    {
-      "data": "<!doctype html><script></s",
-      "errors": [
-        "(1,26): expected-named-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "doctype": true,
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "</s",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head><script></s</script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script></s</script>"
-      }
-    },
-    {
-      "data": "<!doctype html><script></sc",
-      "errors": [
-        "(1,27): expected-named-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "doctype": true,
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "</sc",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head><script></sc</script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script></sc</script>"
-      }
-    },
-    {
-      "data": "<!doctype html><script></scr",
-      "errors": [
-        "(1,28): expected-named-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "doctype": true,
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "</scr",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head><script></scr</script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script></scr</script>"
-      }
-    },
-    {
-      "data": "<!doctype html><script></scri",
-      "errors": [
-        "(1,29): expected-named-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "doctype": true,
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "</scri",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head><script></scri</script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script></scri</script>"
-      }
-    },
-    {
-      "data": "<!doctype html><script></scrip",
-      "errors": [
-        "(1,30): expected-named-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "doctype": true,
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "</scrip",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head><script></scrip</script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script></scrip</script>"
-      }
-    },
-    {
-      "data": "<!doctype html><script></script",
-      "errors": [
-        "(1,31): expected-named-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "doctype": true,
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "</script",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head><script></script</script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script></script</script>"
-      }
-    },
-    {
-      "data": "<!doctype html><script></script ",
-      "errors": [
-        "(1,32): expected-attribute-name-but-got-eof",
-        "(1,32): expected-named-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script"
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head><script></script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script></script>"
-      }
-    },
-    {
-      "data": "<!doctype html><script><!",
-      "errors": [
-        "(1,25): expected-script-data-but-got-eof",
-        "(1,25): expected-named-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "doctype": true,
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "<!",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head><script><!</script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script><!</script>"
-      }
-    },
-    {
-      "data": "<!doctype html><script><!a",
-      "errors": [
-        "(1,26): expected-named-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "doctype": true,
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "<!a",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head><script><!a</script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script><!a</script>"
-      }
-    },
-    {
-      "data": "<!doctype html><script><!-",
-      "errors": [
-        "(1,26): expected-named-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "doctype": true,
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "<!-",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head><script><!-</script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script><!-</script>"
-      }
-    },
-    {
-      "data": "<!doctype html><script><!-a",
-      "errors": [
-        "(1,27): expected-named-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "doctype": true,
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "<!-a",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head><script><!-a</script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script><!-a</script>"
-      }
-    },
-    {
-      "data": "<!doctype html><script><!--",
-      "errors": [
-        "(1,27): expected-named-closing-tag-but-got-eof",
-        "(1,27): unexpected-eof-in-text-mode"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "doctype": true,
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "<!--",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head><script><!--</script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script><!--</script>"
-      }
-    },
-    {
-      "data": "<!doctype html><script><!--a",
-      "errors": [
-        "(1,28): expected-named-closing-tag-but-got-eof",
-        "(1,28): unexpected-eof-in-text-mode"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "doctype": true,
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "<!--a",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head><script><!--a</script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script><!--a</script>"
-      }
-    },
-    {
-      "data": "<!doctype html><script><!--<",
-      "errors": [
-        "(1,28): expected-named-closing-tag-but-got-eof",
-        "(1,28): unexpected-eof-in-text-mode"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "doctype": true,
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "<!--<",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head><script><!--<</script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script><!--<</script>"
-      }
-    },
-    {
-      "data": "<!doctype html><script><!--<a",
-      "errors": [
-        "(1,29): expected-named-closing-tag-but-got-eof",
-        "(1,29): unexpected-eof-in-text-mode"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "doctype": true,
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "<!--<a",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head><script><!--<a</script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script><!--<a</script>"
-      }
-    },
-    {
-      "data": "<!doctype html><script><!--</",
-      "errors": [
-        "(1,29): expected-named-closing-tag-but-got-eof",
-        "(1,29): unexpected-eof-in-text-mode"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "doctype": true,
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "<!--</",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head><script><!--</</script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script><!--</</script>"
-      }
-    },
-    {
-      "data": "<!doctype html><script><!--</script",
-      "errors": [
-        "(1,35): expected-named-closing-tag-but-got-eof",
-        "(1,35): unexpected-eof-in-text-mode"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "doctype": true,
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "<!--</script",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head><script><!--</script</script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script><!--</script</script>"
-      }
-    },
-    {
-      "data": "<!doctype html><script><!--</script ",
-      "errors": [
-        "(1,36): expected-attribute-name-but-got-eof",
-        "(1,36): expected-named-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "doctype": true,
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "<!--",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head><script><!--</script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script><!--</script>"
-      }
-    },
-    {
-      "data": "<!doctype html><script><!--<s",
-      "errors": [
-        "(1,29): expected-named-closing-tag-but-got-eof",
-        "(1,29): unexpected-eof-in-text-mode"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "doctype": true,
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "<!--<s",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head><script><!--<s</script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script><!--<s</script>"
-      }
-    },
-    {
-      "data": "<!doctype html><script><!--<script",
-      "errors": [
-        "(1,34): expected-named-closing-tag-but-got-eof",
-        "(1,34): unexpected-eof-in-text-mode"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "doctype": true,
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "<!--<script",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head><script><!--<script</script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script><!--<script</script>"
-      }
-    },
-    {
-      "data": "<!doctype html><script><!--<script ",
-      "errors": [
-        "(1,35): eof-in-script-in-script",
-        "(1,35): expected-named-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "doctype": true,
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "<!--<script ",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head><script><!--<script </script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script><!--<script </script>"
-      }
-    },
-    {
-      "data": "<!doctype html><script><!--<script <",
-      "errors": [
-        "(1,36): eof-in-script-in-script",
-        "(1,36): expected-named-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "doctype": true,
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "<!--<script <",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head><script><!--<script <</script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script><!--<script <</script>"
-      }
-    },
-    {
-      "data": "<!doctype html><script><!--<script <a",
-      "errors": [
-        "(1,37): eof-in-script-in-script",
-        "(1,37): expected-named-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "doctype": true,
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "<!--<script <a",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head><script><!--<script <a</script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script><!--<script <a</script>"
-      }
-    },
-    {
-      "data": "<!doctype html><script><!--<script </",
-      "errors": [
-        "(1,37): eof-in-script-in-script",
-        "(1,37): expected-named-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "doctype": true,
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "<!--<script </",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head><script><!--<script </</script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script><!--<script </</script>"
-      }
-    },
-    {
-      "data": "<!doctype html><script><!--<script </s",
-      "errors": [
-        "(1,38): eof-in-script-in-script",
-        "(1,38): expected-named-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "doctype": true,
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "<!--<script </s",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head><script><!--<script </s</script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script><!--<script </s</script>"
-      }
-    },
-    {
-      "data": "<!doctype html><script><!--<script </script",
-      "errors": [
-        "(1,43): eof-in-script-in-script",
-        "(1,43): expected-named-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "doctype": true,
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "<!--<script </script",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head><script><!--<script </script</script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script><!--<script </script</script>"
-      }
-    },
-    {
-      "data": "<!doctype html><script><!--<script </scripta",
-      "errors": [
-        "(1,44): eof-in-script-in-script",
-        "(1,44): expected-named-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "doctype": true,
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "<!--<script </scripta",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head><script><!--<script </scripta</script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script><!--<script </scripta</script>"
-      }
-    },
-    {
-      "data": "<!doctype html><script><!--<script </script ",
-      "errors": [
-        "(1,44): expected-named-closing-tag-but-got-eof",
-        "(1,44): unexpected-eof-in-text-mode"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "doctype": true,
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "<!--<script </script ",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head><script><!--<script </script </script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script><!--<script </script </script>"
-      }
-    },
-    {
-      "data": "<!doctype html><script><!--<script </script>",
-      "errors": [
-        "(1,44): expected-named-closing-tag-but-got-eof",
-        "(1,44): unexpected-eof-in-text-mode"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "doctype": true,
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "<!--<script </script>",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head><script><!--<script </script></script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script><!--<script </script></script>"
-      }
-    },
-    {
-      "data": "<!doctype html><script><!--<script </script/",
-      "errors": [
-        "(1,44): expected-named-closing-tag-but-got-eof",
-        "(1,44): unexpected-eof-in-text-mode"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "doctype": true,
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "<!--<script </script/",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head><script><!--<script </script/</script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script><!--<script </script/</script>"
-      }
-    },
-    {
-      "data": "<!doctype html><script><!--<script </script <",
-      "errors": [
-        "(1,45): expected-named-closing-tag-but-got-eof",
-        "(1,45): unexpected-eof-in-text-mode"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "doctype": true,
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "<!--<script </script <",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head><script><!--<script </script <</script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script><!--<script </script <</script>"
-      }
-    },
-    {
-      "data": "<!doctype html><script><!--<script </script <a",
-      "errors": [
-        "(1,46): expected-named-closing-tag-but-got-eof",
-        "(1,46): unexpected-eof-in-text-mode"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "doctype": true,
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "<!--<script </script <a",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head><script><!--<script </script <a</script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script><!--<script </script <a</script>"
-      }
-    },
-    {
-      "data": "<!doctype html><script><!--<script </script </",
-      "errors": [
-        "(1,46): expected-named-closing-tag-but-got-eof",
-        "(1,46): unexpected-eof-in-text-mode"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "doctype": true,
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "<!--<script </script </",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head><script><!--<script </script </</script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script><!--<script </script </</script>"
-      }
-    },
-    {
-      "data": "<!doctype html><script><!--<script </script </script",
-      "errors": [
-        "(1,52): expected-named-closing-tag-but-got-eof",
-        "(1,52): unexpected-eof-in-text-mode"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "doctype": true,
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "<!--<script </script </script",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head><script><!--<script </script </script</script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script><!--<script </script </script</script>"
-      }
-    },
-    {
-      "data": "<!doctype html><script><!--<script </script </script ",
-      "errors": [
-        "(1,53): expected-attribute-name-but-got-eof",
-        "(1,53): expected-named-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "doctype": true,
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "<!--<script </script ",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head><script><!--<script </script </script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script><!--<script </script </script>"
-      }
-    },
-    {
-      "data": "<!doctype html><script><!--<script </script </script/",
-      "errors": [
-        "(1,53): unexpected-EOF-after-solidus-in-tag",
-        "(1,53): expected-named-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "doctype": true,
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "<!--<script </script ",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head><script><!--<script </script </script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script><!--<script </script </script>"
-      }
-    },
-    {
-      "data": "<!doctype html><script><!--<script </script </script>",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "doctype": true,
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "<!--<script </script ",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head><script><!--<script </script </script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script><!--<script </script </script>"
-      }
-    },
-    {
-      "data": "<!doctype html><script><!--<script -",
-      "errors": [
-        "(1,36): eof-in-script-in-script",
-        "(1,36): expected-named-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "doctype": true,
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "<!--<script -",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head><script><!--<script -</script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script><!--<script -</script>"
-      }
-    },
-    {
-      "data": "<!doctype html><script><!--<script -a",
-      "errors": [
-        "(1,37): eof-in-script-in-script",
-        "(1,37): expected-named-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "doctype": true,
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "<!--<script -a",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head><script><!--<script -a</script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script><!--<script -a</script>"
-      }
-    },
-    {
-      "data": "<!doctype html><script><!--<script -<",
-      "errors": [
-        "(1,37): eof-in-script-in-script",
-        "(1,37): expected-named-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "doctype": true,
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "<!--<script -<",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head><script><!--<script -<</script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script><!--<script -<</script>"
-      }
-    },
-    {
-      "data": "<!doctype html><script><!--<script --",
-      "errors": [
-        "(1,37): eof-in-script-in-script",
-        "(1,37): expected-named-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "doctype": true,
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "<!--<script --",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head><script><!--<script --</script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script><!--<script --</script>"
-      }
-    },
-    {
-      "data": "<!doctype html><script><!--<script --a",
-      "errors": [
-        "(1,38): eof-in-script-in-script",
-        "(1,38): expected-named-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "doctype": true,
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "<!--<script --a",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head><script><!--<script --a</script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script><!--<script --a</script>"
-      }
-    },
-    {
-      "data": "<!doctype html><script><!--<script --<",
-      "errors": [
-        "(1,38): eof-in-script-in-script",
-        "(1,38): expected-named-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "doctype": true,
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "<!--<script --<",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head><script><!--<script --<</script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script><!--<script --<</script>"
-      }
-    },
-    {
-      "data": "<!doctype html><script><!--<script -->",
-      "errors": [
-        "(1,38): expected-named-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "doctype": true,
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "<!--<script -->",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head><script><!--<script --></script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script><!--<script --></script>"
-      }
-    },
-    {
-      "data": "<!doctype html><script><!--<script --><",
-      "errors": [
-        "(1,39): expected-named-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "doctype": true,
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "<!--<script --><",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head><script><!--<script --><</script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script><!--<script --><</script>"
-      }
-    },
-    {
-      "data": "<!doctype html><script><!--<script --></",
-      "errors": [
-        "(1,40): expected-named-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "doctype": true,
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "<!--<script --></",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head><script><!--<script --></</script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script><!--<script --></</script>"
-      }
-    },
-    {
-      "data": "<!doctype html><script><!--<script --></script",
-      "errors": [
-        "(1,46): expected-named-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "doctype": true,
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "<!--<script --></script",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head><script><!--<script --></script</script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script><!--<script --></script</script>"
-      }
-    },
-    {
-      "data": "<!doctype html><script><!--<script --></script ",
-      "errors": [
-        "(1,47): expected-attribute-name-but-got-eof",
-        "(1,47): expected-named-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "doctype": true,
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "<!--<script -->",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head><script><!--<script --></script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script><!--<script --></script>"
-      }
-    },
-    {
-      "data": "<!doctype html><script><!--<script --></script/",
-      "errors": [
-        "(1,47): unexpected-EOF-after-solidus-in-tag",
-        "(1,47): expected-named-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "doctype": true,
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "<!--<script -->",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head><script><!--<script --></script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script><!--<script --></script>"
-      }
-    },
-    {
-      "data": "<!doctype html><script><!--<script --></script>",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "doctype": true,
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "<!--<script -->",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head><script><!--<script --></script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script><!--<script --></script>"
-      }
-    },
-    {
-      "data": "<!doctype html><script><!--<script><\\/script>--></script>",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "doctype": true,
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "<!--<script><\\/script>-->",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head><script><!--<script><\\/script>--></script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script><!--<script><\\/script>--></script>"
-      }
-    },
-    {
-      "data": "<!doctype html><script><!--<script></scr'+'ipt>--></script>",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "doctype": true,
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "<!--<script></scr'+'ipt>-->",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head><script><!--<script></scr'+'ipt>--></script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script><!--<script></scr'+'ipt>--></script>"
-      }
-    },
-    {
-      "data": "<!doctype html><script><!--<script></script><script></script></script>",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "doctype": true,
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "<!--<script></script><script></script>",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head><script><!--<script></script><script></script></script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script><!--<script></script><script></script></script>"
-      }
-    },
-    {
-      "data": "<!doctype html><script><!--<script></script><script></script>--><!--</script>",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "doctype": true,
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "<!--<script></script><script></script>--><!--",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head><script><!--<script></script><script></script>--><!--</script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script><!--<script></script><script></script>--><!--</script>"
-      }
-    },
-    {
-      "data": "<!doctype html><script><!--<script></script><script></script>-- ></script>",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "doctype": true,
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "<!--<script></script><script></script>-- >",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head><script><!--<script></script><script></script>-- ></script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script><!--<script></script><script></script>-- ></script>"
-      }
-    },
-    {
-      "data": "<!doctype html><script><!--<script></script><script></script>- -></script>",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "doctype": true,
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "<!--<script></script><script></script>- ->",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head><script><!--<script></script><script></script>- -></script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script><!--<script></script><script></script>- -></script>"
-      }
-    },
-    {
-      "data": "<!doctype html><script><!--<script></script><script></script>- - ></script>",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "doctype": true,
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "<!--<script></script><script></script>- - >",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head><script><!--<script></script><script></script>- - ></script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script><!--<script></script><script></script>- - ></script>"
-      }
-    },
-    {
-      "data": "<!doctype html><script><!--<script></script><script></script>-></script>",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "doctype": true,
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "<!--<script></script><script></script>->",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head><script><!--<script></script><script></script>-></script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script><!--<script></script><script></script>-></script>"
-      }
-    },
-    {
-      "data": "<!doctype html><script><!--<script>--!></script>X",
-      "errors": [
-        "(1,49): expected-named-closing-tag-but-got-eof",
-        "(1,49): unexpected-EOF-in-text-mode"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "doctype": true,
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "<!--<script>--!></script>X",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head><script><!--<script>--!></script>X</script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script><!--<script>--!></script>X</script>"
-      }
-    },
-    {
-      "data": "<!doctype html><script><!--<scr'+'ipt></script>--></script>",
-      "errors": [
-        "(1,59): unexpected-end-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "doctype": true,
-          "no_escape": true,
-          "escaped": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "<!--<scr'+'ipt>",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "-->",
-                    "escaped": true
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head><script><!--<scr'+'ipt></script></head><body>--&gt;</body></html>",
-        "noQuirksBodyHtml": "<script><!--<scr'+'ipt></script>--&gt;"
-      }
-    },
-    {
-      "data": "<!doctype html><script><!--<script></scr'+'ipt></script>X",
-      "errors": [
-        "(1,57): expected-named-closing-tag-but-got-eof",
-        "(1,57): unexpected-eof-in-text-mode"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "doctype": true,
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "<!--<script></scr'+'ipt></script>X",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head><script><!--<script></scr'+'ipt></script>X</script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script><!--<script></scr'+'ipt></script>X</script>"
-      }
-    },
-    {
-      "data": "<!doctype html><style><!--<style></style>--></style>",
-      "errors": [
-        "(1,52): unexpected-end-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "style": true,
-            "body": true
-          },
-          "doctype": true,
-          "no_escape": true,
-          "escaped": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "style",
-                    "children": [
-                      {
-                        "text": "<!--<style>",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "-->",
-                    "escaped": true
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head><style><!--<style></style></head><body>--&gt;</body></html>",
-        "noQuirksBodyHtml": "<style><!--<style></style>--&gt;"
-      }
-    },
-    {
-      "data": "<!doctype html><style><!--</style>X",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "style": true,
-            "body": true
-          },
-          "doctype": true,
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "style",
-                    "children": [
-                      {
-                        "text": "<!--",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "X"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head><style><!--</style></head><body>X</body></html>",
-        "noQuirksBodyHtml": "<style><!--</style>X"
-      }
-    },
-    {
-      "data": "<!doctype html><style><!--...</style>...--></style>",
-      "errors": [
-        "(1,51): unexpected-end-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "style": true,
-            "body": true
-          },
-          "doctype": true,
-          "no_escape": true,
-          "escaped": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "style",
-                    "children": [
-                      {
-                        "text": "<!--...",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "...-->",
-                    "escaped": true
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head><style><!--...</style></head><body>...--&gt;</body></html>",
-        "noQuirksBodyHtml": "<style><!--...</style>...--&gt;"
-      }
-    },
-    {
-      "data": "<!doctype html><style><!--<br><html xmlns:v=\"urn:schemas-microsoft-com:vml\"><!--[if !mso]><style></style>X",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "style": true,
-            "body": true
-          },
-          "doctype": true,
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "style",
-                    "children": [
-                      {
-                        "text": "<!--<br><html xmlns:v=\"urn:schemas-microsoft-com:vml\"><!--[if !mso]><style>",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "X"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head><style><!--<br><html xmlns:v=\"urn:schemas-microsoft-com:vml\"><!--[if !mso]><style></style></head><body>X</body></html>",
-        "noQuirksBodyHtml": "<style><!--<br><html xmlns:v=\"urn:schemas-microsoft-com:vml\"><!--[if !mso]><style></style>X"
-      }
-    },
-    {
-      "data": "<!doctype html><style><!--...<style><!--...--!></style>--></style>",
-      "errors": [
-        "(1,66): unexpected-end-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "style": true,
-            "body": true
-          },
-          "doctype": true,
-          "no_escape": true,
-          "escaped": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "style",
-                    "children": [
-                      {
-                        "text": "<!--...<style><!--...--!>",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "-->",
-                    "escaped": true
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head><style><!--...<style><!--...--!></style></head><body>--&gt;</body></html>",
-        "noQuirksBodyHtml": "<style><!--...<style><!--...--!></style>--&gt;"
-      }
-    },
-    {
-      "data": "<!doctype html><style><!--...</style><!-- --><style>@import ...</style>",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "style": true,
-            "body": true
-          },
-          "doctype": true,
-          "no_escape": true,
-          "comment": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "style",
-                    "children": [
-                      {
-                        "text": "<!--...",
-                        "no_escape": true
-                      }
-                    ]
-                  },
-                  {
-                    "comment": " "
-                  },
-                  {
-                    "tag": "style",
-                    "children": [
-                      {
-                        "text": "@import ...",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head><style><!--...</style><!-- --><style>@import ...</style></head><body></body></html>",
-        "noQuirksBodyHtml": "<style><!--...</style><!-- --><style>@import ...</style>"
-      }
-    },
-    {
-      "data": "<!doctype html><style>...<style><!--...</style><!-- --></style>",
-      "errors": [
-        "(1,63): unexpected-end-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "style": true,
-            "body": true
-          },
-          "doctype": true,
-          "no_escape": true,
-          "comment": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "style",
-                    "children": [
-                      {
-                        "text": "...<style><!--...",
-                        "no_escape": true
-                      }
-                    ]
-                  },
-                  {
-                    "comment": " "
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head><style>...<style><!--...</style><!-- --></head><body></body></html>",
-        "noQuirksBodyHtml": "<style>...<style><!--...</style><!-- -->"
-      }
-    },
-    {
-      "data": "<!doctype html><style>...<!--[if IE]><style>...</style>X",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "style": true,
-            "body": true
-          },
-          "doctype": true,
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "style",
-                    "children": [
-                      {
-                        "text": "...<!--[if IE]><style>...",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "X"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head><style>...<!--[if IE]><style>...</style></head><body>X</body></html>",
-        "noQuirksBodyHtml": "<style>...<!--[if IE]><style>...</style>X"
-      }
-    },
-    {
-      "data": "<!doctype html><title><!--<title></title>--></title>",
-      "errors": [
-        "(1,52): unexpected-end-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "title": true,
-            "body": true
-          },
-          "doctype": true,
-          "escaped": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "title",
-                    "children": [
-                      {
-                        "text": "<!--<title>",
-                        "escaped": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "-->",
-                    "escaped": true
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head><title>&lt;!--&lt;title&gt;</title></head><body>--&gt;</body></html>",
-        "noQuirksBodyHtml": "<title>&lt;!--&lt;title&gt;</title>--&gt;"
-      }
-    },
-    {
-      "data": "<!doctype html><title>&lt;/title></title>",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "title": true,
-            "body": true
-          },
-          "doctype": true,
-          "escaped": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "title",
-                    "children": [
-                      {
-                        "text": "</title>",
-                        "escaped": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head><title>&lt;/title&gt;</title></head><body></body></html>",
-        "noQuirksBodyHtml": "<title>&lt;/title&gt;</title>"
-      }
-    },
-    {
-      "data": "<!doctype html><title>foo/title><link></head><body>X",
-      "errors": [
-        "(1,52): expected-named-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "title": true,
-            "body": true
-          },
-          "doctype": true,
-          "escaped": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "title",
-                    "children": [
-                      {
-                        "text": "foo/title><link></head><body>X",
-                        "escaped": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head><title>foo/title&gt;&lt;link&gt;&lt;/head&gt;&lt;body&gt;X</title></head><body></body></html>",
-        "noQuirksBodyHtml": "<title>foo/title&gt;&lt;link&gt;&lt;/head&gt;&lt;body&gt;X</title>"
-      }
-    },
-    {
-      "data": "<!doctype html><noscript><!--<noscript></noscript>--></noscript>",
-      "errors": [
-        "(1,64): unexpected-end-tag"
-      ],
-      "script": "on",
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "noscript": true,
-            "body": true
-          },
-          "doctype": true,
-          "no_escape": true,
-          "escaped": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "noscript",
-                    "children": [
-                      {
-                        "text": "<!--<noscript>",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "-->",
-                    "escaped": true
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head><noscript><!--<noscript></noscript></head><body>--&gt;</body></html>",
-        "noQuirksBodyHtml": "<noscript><!--<noscript></noscript>--></noscript>"
-      }
-    },
-    {
-      "data": "<!doctype html><noscript><!--<noscript></noscript>--></noscript>",
-      "errors": [],
-      "script": "off",
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "noscript": true,
-            "body": true
-          },
-          "doctype": true,
-          "comment": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "noscript",
-                    "children": [
-                      {
-                        "comment": "<noscript></noscript>"
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head><noscript><!--<noscript></noscript>--></noscript></head><body></body></html>",
-        "noQuirksBodyHtml": "<noscript><!--<noscript></noscript>--></noscript>"
-      }
-    },
-    {
-      "data": "<!doctype html><noscript><!--</noscript>X<noscript>--></noscript>",
-      "errors": [],
-      "script": "on",
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "noscript": true,
-            "body": true
-          },
-          "doctype": true,
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "noscript",
-                    "children": [
-                      {
-                        "text": "<!--",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "X"
-                  },
-                  {
-                    "tag": "noscript",
-                    "children": [
-                      {
-                        "text": "-->",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head><noscript><!--</noscript></head><body>X<noscript>--></noscript></body></html>",
-        "noQuirksBodyHtml": "<noscript><!--</noscript>X<noscript>--></noscript>"
-      }
-    },
-    {
-      "data": "<!doctype html><noscript><!--</noscript>X<noscript>--></noscript>",
-      "errors": [],
-      "script": "off",
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "noscript": true,
-            "body": true
-          },
-          "doctype": true,
-          "comment": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "noscript",
-                    "children": [
-                      {
-                        "comment": "</noscript>X<noscript>"
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head><noscript><!--</noscript>X<noscript>--></noscript></head><body></body></html>",
-        "noQuirksBodyHtml": "<noscript><!--</noscript>X<noscript>--></noscript>"
-      }
-    },
-    {
-      "data": "<!doctype html><noscript><iframe></noscript>X",
-      "errors": [],
-      "script": "on",
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "noscript": true,
-            "body": true
-          },
-          "doctype": true,
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "noscript",
-                    "children": [
-                      {
-                        "text": "<iframe>",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "X"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head><noscript><iframe></noscript></head><body>X</body></html>",
-        "noQuirksBodyHtml": "<noscript><iframe></noscript>X</iframe></noscript>"
-      }
-    },
-    {
-      "data": "<!doctype html><noscript><iframe></noscript>X",
-      "errors": [
-        " * (1,34) unexpected token in head noscript",
-        " * (1,46) unexpected EOF"
-      ],
-      "script": "off",
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "noscript": true,
-            "body": true,
-            "iframe": true
-          },
-          "doctype": true,
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "noscript"
-                  }
-                ]
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "iframe",
-                    "children": [
-                      {
-                        "text": "</noscript>X",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head><noscript></noscript></head><body><iframe></noscript>X</iframe></body></html>",
-        "noQuirksBodyHtml": "<noscript><iframe></noscript>X</iframe></noscript>"
-      }
-    },
-    {
-      "data": "<!doctype html><noframes><!--<noframes></noframes>--></noframes>",
-      "errors": [
-        "(1,64): unexpected-end-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "noframes": true,
-            "body": true
-          },
-          "doctype": true,
-          "no_escape": true,
-          "escaped": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "noframes",
-                    "children": [
-                      {
-                        "text": "<!--<noframes>",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "-->",
-                    "escaped": true
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head><noframes><!--<noframes></noframes></head><body>--&gt;</body></html>",
-        "noQuirksBodyHtml": "<noframes><!--<noframes></noframes>--&gt;"
-      }
-    },
-    {
-      "data": "<!doctype html><noframes><body><script><!--...</script></body></noframes></html>",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "noframes": true,
-            "body": true
-          },
-          "doctype": true,
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "noframes",
-                    "children": [
-                      {
-                        "text": "<body><script><!--...</script></body>",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head><noframes><body><script><!--...</script></body></noframes></head><body></body></html>",
-        "noQuirksBodyHtml": "<noframes><body><script><!--...</script></body></noframes>"
-      }
-    },
-    {
-      "data": "<!doctype html><textarea><!--<textarea></textarea>--></textarea>",
-      "errors": [
-        "(1,64): unexpected-end-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "textarea": true
-          },
-          "doctype": true,
-          "escaped": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "textarea",
-                    "children": [
-                      {
-                        "text": "<!--<textarea>",
-                        "escaped": true
-                      }
-                    ]
-                  },
-                  {
-                    "text": "-->",
-                    "escaped": true
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><textarea>&lt;!--&lt;textarea&gt;</textarea>--&gt;</body></html>",
-        "noQuirksBodyHtml": "<textarea>&lt;!--&lt;textarea&gt;</textarea>--&gt;"
-      }
-    },
-    {
-      "data": "<!doctype html><textarea>&lt;/textarea></textarea>",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "textarea": true
-          },
-          "doctype": true,
-          "escaped": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "textarea",
-                    "children": [
-                      {
-                        "text": "</textarea>",
-                        "escaped": true
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><textarea>&lt;/textarea&gt;</textarea></body></html>",
-        "noQuirksBodyHtml": "<textarea>&lt;/textarea&gt;</textarea>"
-      }
-    },
-    {
-      "data": "<!doctype html><textarea>&lt;</textarea>",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "textarea": true
-          },
-          "doctype": true,
-          "escaped": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "textarea",
-                    "children": [
-                      {
-                        "text": "<",
-                        "escaped": true
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><textarea>&lt;</textarea></body></html>",
-        "noQuirksBodyHtml": "<textarea>&lt;</textarea>"
-      }
-    },
-    {
-      "data": "<!doctype html><textarea>a&lt;b</textarea>",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "textarea": true
-          },
-          "doctype": true,
-          "escaped": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "textarea",
-                    "children": [
-                      {
-                        "text": "a<b",
-                        "escaped": true
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><textarea>a&lt;b</textarea></body></html>",
-        "noQuirksBodyHtml": "<textarea>a&lt;b</textarea>"
-      }
-    },
-    {
-      "data": "<!doctype html><iframe><!--<iframe></iframe>--></iframe>",
-      "errors": [
-        "(1,56): unexpected-end-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "iframe": true
-          },
-          "doctype": true,
-          "no_escape": true,
-          "escaped": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "iframe",
-                    "children": [
-                      {
-                        "text": "<!--<iframe>",
-                        "no_escape": true
-                      }
-                    ]
-                  },
-                  {
-                    "text": "-->",
-                    "escaped": true
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><iframe><!--<iframe></iframe>--&gt;</body></html>",
-        "noQuirksBodyHtml": "<iframe><!--<iframe></iframe>--&gt;"
-      }
-    },
-    {
-      "data": "<!doctype html><iframe>...<!--X->...<!--/X->...</iframe>",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "iframe": true
-          },
-          "doctype": true,
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "iframe",
-                    "children": [
-                      {
-                        "text": "...<!--X->...<!--/X->...",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><iframe>...<!--X->...<!--/X->...</iframe></body></html>",
-        "noQuirksBodyHtml": "<iframe>...<!--X->...<!--/X->...</iframe>"
-      }
-    },
-    {
-      "data": "<!doctype html><xmp><!--<xmp></xmp>--></xmp>",
-      "errors": [
-        "(1,44): unexpected-end-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "xmp": true
-          },
-          "doctype": true,
-          "no_escape": true,
-          "escaped": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "xmp",
-                    "children": [
-                      {
-                        "text": "<!--<xmp>",
-                        "no_escape": true
-                      }
-                    ]
-                  },
-                  {
-                    "text": "-->",
-                    "escaped": true
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><xmp><!--<xmp></xmp>--&gt;</body></html>",
-        "noQuirksBodyHtml": "<xmp><!--<xmp></xmp>--&gt;"
-      }
-    },
-    {
-      "data": "<!doctype html><noembed><!--<noembed></noembed>--></noembed>",
-      "errors": [
-        "(1,60): unexpected-end-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "noembed": true
-          },
-          "doctype": true,
-          "no_escape": true,
-          "escaped": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "noembed",
-                    "children": [
-                      {
-                        "text": "<!--<noembed>",
-                        "no_escape": true
-                      }
-                    ]
-                  },
-                  {
-                    "text": "-->",
-                    "escaped": true
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><noembed><!--<noembed></noembed>--&gt;</body></html>",
-        "noQuirksBodyHtml": "<noembed><!--<noembed></noembed>--&gt;"
-      }
-    },
-    {
-      "data": "<script>",
-      "errors": [
-        "(1,8): expected-doctype-but-got-start-tag",
-        "(1,8): expected-named-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script"
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><script></script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script></script>"
-      }
-    },
-    {
-      "data": "<script>a",
-      "errors": [
-        "(1,8): expected-doctype-but-got-start-tag",
-        "(1,9): expected-named-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "a",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><script>a</script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script>a</script>"
-      }
-    },
-    {
-      "data": "<script><",
-      "errors": [
-        "(1,8): expected-doctype-but-got-start-tag",
-        "(1,9): expected-named-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "<",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><script><</script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script><</script>"
-      }
-    },
-    {
-      "data": "<script></",
-      "errors": [
-        "(1,8): expected-doctype-but-got-start-tag",
-        "(1,10): expected-named-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "</",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><script></</script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script></</script>"
-      }
-    },
-    {
-      "data": "<script></S",
-      "errors": [
-        "(1,8): expected-doctype-but-got-start-tag",
-        "(1,11): expected-named-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "</S",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><script></S</script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script></S</script>"
-      }
-    },
-    {
-      "data": "<script></SC",
-      "errors": [
-        "(1,8): expected-doctype-but-got-start-tag",
-        "(1,12): expected-named-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "</SC",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><script></SC</script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script></SC</script>"
-      }
-    },
-    {
-      "data": "<script></SCR",
-      "errors": [
-        "(1,8): expected-doctype-but-got-start-tag",
-        "(1,13): expected-named-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "</SCR",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><script></SCR</script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script></SCR</script>"
-      }
-    },
-    {
-      "data": "<script></SCRI",
-      "errors": [
-        "(1,8): expected-doctype-but-got-start-tag",
-        "(1,14): expected-named-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "</SCRI",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><script></SCRI</script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script></SCRI</script>"
-      }
-    },
-    {
-      "data": "<script></SCRIP",
-      "errors": [
-        "(1,8): expected-doctype-but-got-start-tag",
-        "(1,15): expected-named-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "</SCRIP",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><script></SCRIP</script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script></SCRIP</script>"
-      }
-    },
-    {
-      "data": "<script></SCRIPT",
-      "errors": [
-        "(1,8): expected-doctype-but-got-start-tag",
-        "(1,16): expected-named-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "</SCRIPT",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><script></SCRIPT</script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script></SCRIPT</script>"
-      }
-    },
-    {
-      "data": "<script></SCRIPT ",
-      "errors": [
-        "(1,8): expected-doctype-but-got-start-tag",
-        "(1,17): expected-attribute-name-but-got-eof",
-        "(1,17): expected-named-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script"
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><script></script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script></script>"
-      }
-    },
-    {
-      "data": "<script></s",
-      "errors": [
-        "(1,8): expected-doctype-but-got-start-tag",
-        "(1,11): expected-named-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "</s",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><script></s</script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script></s</script>"
-      }
-    },
-    {
-      "data": "<script></sc",
-      "errors": [
-        "(1,8): expected-doctype-but-got-start-tag",
-        "(1,12): expected-named-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "</sc",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><script></sc</script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script></sc</script>"
-      }
-    },
-    {
-      "data": "<script></scr",
-      "errors": [
-        "(1,8): expected-doctype-but-got-start-tag",
-        "(1,13): expected-named-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "</scr",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><script></scr</script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script></scr</script>"
-      }
-    },
-    {
-      "data": "<script></scri",
-      "errors": [
-        "(1,8): expected-doctype-but-got-start-tag",
-        "(1,14): expected-named-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "</scri",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><script></scri</script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script></scri</script>"
-      }
-    },
-    {
-      "data": "<script></scrip",
-      "errors": [
-        "(1,8): expected-doctype-but-got-start-tag",
-        "(1,15): expected-named-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "</scrip",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><script></scrip</script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script></scrip</script>"
-      }
-    },
-    {
-      "data": "<script></script",
-      "errors": [
-        "(1,8): expected-doctype-but-got-start-tag",
-        "(1,16): expected-named-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "</script",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><script></script</script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script></script</script>"
-      }
-    },
-    {
-      "data": "<script></script ",
-      "errors": [
-        "(1,8): expected-doctype-but-got-start-tag",
-        "(1,17): expected-attribute-name-but-got-eof",
-        "(1,17): expected-named-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script"
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><script></script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script></script>"
-      }
-    },
-    {
-      "data": "<script><!",
-      "errors": [
-        "(1,8): expected-doctype-but-got-start-tag",
-        "(1,10): expected-script-data-but-got-eof",
-        "(1,10): expected-named-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "<!",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><script><!</script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script><!</script>"
-      }
-    },
-    {
-      "data": "<script><!a",
-      "errors": [
-        "(1,8): expected-doctype-but-got-start-tag",
-        "(1,11): expected-named-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "<!a",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><script><!a</script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script><!a</script>"
-      }
-    },
-    {
-      "data": "<script><!-",
-      "errors": [
-        "(1,8): expected-doctype-but-got-start-tag",
-        "(1,11): expected-named-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "<!-",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><script><!-</script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script><!-</script>"
-      }
-    },
-    {
-      "data": "<script><!-a",
-      "errors": [
-        "(1,8): expected-doctype-but-got-start-tag",
-        "(1,12): expected-named-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "<!-a",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><script><!-a</script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script><!-a</script>"
-      }
-    },
-    {
-      "data": "<script><!--",
-      "errors": [
-        "(1,8): expected-doctype-but-got-start-tag",
-        "(1,12): expected-named-closing-tag-but-got-eof",
-        "(1,12): unexpected-eof-in-text-mode"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "<!--",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><script><!--</script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script><!--</script>"
-      }
-    },
-    {
-      "data": "<script><!--a",
-      "errors": [
-        "(1,8): expected-doctype-but-got-start-tag",
-        "(1,13): expected-named-closing-tag-but-got-eof",
-        "(1,13): unexpected-eof-in-text-mode"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "<!--a",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><script><!--a</script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script><!--a</script>"
-      }
-    },
-    {
-      "data": "<script><!--<",
-      "errors": [
-        "(1,8): expected-doctype-but-got-start-tag",
-        "(1,13): expected-named-closing-tag-but-got-eof",
-        "(1,13): unexpected-eof-in-text-mode"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "<!--<",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><script><!--<</script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script><!--<</script>"
-      }
-    },
-    {
-      "data": "<script><!--<a",
-      "errors": [
-        "(1,8): expected-doctype-but-got-start-tag",
-        "(1,14): expected-named-closing-tag-but-got-eof",
-        "(1,14): unexpected-eof-in-text-mode"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "<!--<a",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><script><!--<a</script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script><!--<a</script>"
-      }
-    },
-    {
-      "data": "<script><!--</",
-      "errors": [
-        "(1,8): expected-doctype-but-got-start-tag",
-        "(1,14): expected-named-closing-tag-but-got-eof",
-        "(1,14): unexpected-eof-in-text-mode"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "<!--</",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><script><!--</</script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script><!--</</script>"
-      }
-    },
-    {
-      "data": "<script><!--</script",
-      "errors": [
-        "(1,8): expected-doctype-but-got-start-tag",
-        "(1,20): expected-named-closing-tag-but-got-eof",
-        "(1,20): unexpected-eof-in-text-mode"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "<!--</script",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><script><!--</script</script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script><!--</script</script>"
-      }
-    },
-    {
-      "data": "<script><!--</script ",
-      "errors": [
-        "(1,8): expected-doctype-but-got-start-tag",
-        "(1,21): expected-attribute-name-but-got-eof",
-        "(1,21): expected-named-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "<!--",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><script><!--</script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script><!--</script>"
-      }
-    },
-    {
-      "data": "<script><!--<s",
-      "errors": [
-        "(1,8): expected-doctype-but-got-start-tag",
-        "(1,14): expected-named-closing-tag-but-got-eof",
-        "(1,14): unexpected-eof-in-text-mode"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "<!--<s",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><script><!--<s</script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script><!--<s</script>"
-      }
-    },
-    {
-      "data": "<script><!--<script",
-      "errors": [
-        "(1,8): expected-doctype-but-got-start-tag",
-        "(1,19): expected-named-closing-tag-but-got-eof",
-        "(1,19): unexpected-eof-in-text-mode"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "<!--<script",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><script><!--<script</script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script><!--<script</script>"
-      }
-    },
-    {
-      "data": "<script><!--<script ",
-      "errors": [
-        "(1,8): expected-doctype-but-got-start-tag",
-        "(1,20): eof-in-script-in-script",
-        "(1,20): expected-named-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "<!--<script ",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><script><!--<script </script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script><!--<script </script>"
-      }
-    },
-    {
-      "data": "<script><!--<script <",
-      "errors": [
-        "(1,8): expected-doctype-but-got-start-tag",
-        "(1,21): eof-in-script-in-script",
-        "(1,21): expected-named-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "<!--<script <",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><script><!--<script <</script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script><!--<script <</script>"
-      }
-    },
-    {
-      "data": "<script><!--<script <a",
-      "errors": [
-        "(1,8): expected-doctype-but-got-start-tag",
-        "(1,22): eof-in-script-in-script",
-        "(1,22): expected-named-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "<!--<script <a",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><script><!--<script <a</script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script><!--<script <a</script>"
-      }
-    },
-    {
-      "data": "<script><!--<script </",
-      "errors": [
-        "(1,8): expected-doctype-but-got-start-tag",
-        "(1,22): eof-in-script-in-script",
-        "(1,22): expected-named-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "<!--<script </",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><script><!--<script </</script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script><!--<script </</script>"
-      }
-    },
-    {
-      "data": "<script><!--<script </s",
-      "errors": [
-        "(1,8): expected-doctype-but-got-start-tag",
-        "(1,23): eof-in-script-in-script",
-        "(1,23): expected-named-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "<!--<script </s",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><script><!--<script </s</script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script><!--<script </s</script>"
-      }
-    },
-    {
-      "data": "<script><!--<script </script",
-      "errors": [
-        "(1,8): expected-doctype-but-got-start-tag",
-        "(1,28): eof-in-script-in-script",
-        "(1,28): expected-named-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "<!--<script </script",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><script><!--<script </script</script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script><!--<script </script</script>"
-      }
-    },
-    {
-      "data": "<script><!--<script </scripta",
-      "errors": [
-        "(1,8): expected-doctype-but-got-start-tag",
-        "(1,29): eof-in-script-in-script",
-        "(1,29): expected-named-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "<!--<script </scripta",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><script><!--<script </scripta</script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script><!--<script </scripta</script>"
-      }
-    },
-    {
-      "data": "<script><!--<script </script ",
-      "errors": [
-        "(1,8): expected-doctype-but-got-start-tag",
-        "(1,29): expected-named-closing-tag-but-got-eof",
-        "(1,29): unexpected-eof-in-text-mode"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "<!--<script </script ",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><script><!--<script </script </script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script><!--<script </script </script>"
-      }
-    },
-    {
-      "data": "<script><!--<script </script>",
-      "errors": [
-        "(1,8): expected-doctype-but-got-start-tag",
-        "(1,29): expected-named-closing-tag-but-got-eof",
-        "(1,29): unexpected-eof-in-text-mode"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "<!--<script </script>",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><script><!--<script </script></script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script><!--<script </script></script>"
-      }
-    },
-    {
-      "data": "<script><!--<script </script/",
-      "errors": [
-        "(1,8): expected-doctype-but-got-start-tag",
-        "(1,29): expected-named-closing-tag-but-got-eof",
-        "(1,29): unexpected-eof-in-text-mode"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "<!--<script </script/",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><script><!--<script </script/</script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script><!--<script </script/</script>"
-      }
-    },
-    {
-      "data": "<script><!--<script </script <",
-      "errors": [
-        "(1,8): expected-doctype-but-got-start-tag",
-        "(1,30): expected-named-closing-tag-but-got-eof",
-        "(1,30): unexpected-eof-in-text-mode"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "<!--<script </script <",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><script><!--<script </script <</script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script><!--<script </script <</script>"
-      }
-    },
-    {
-      "data": "<script><!--<script </script <a",
-      "errors": [
-        "(1,8): expected-doctype-but-got-start-tag",
-        "(1,31): expected-named-closing-tag-but-got-eof",
-        "(1,31): unexpected-eof-in-text-mode"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "<!--<script </script <a",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><script><!--<script </script <a</script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script><!--<script </script <a</script>"
-      }
-    },
-    {
-      "data": "<script><!--<script </script </",
-      "errors": [
-        "(1,8): expected-doctype-but-got-start-tag",
-        "(1,31): expected-named-closing-tag-but-got-eof",
-        "(1,31): unexpected-eof-in-text-mode"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "<!--<script </script </",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><script><!--<script </script </</script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script><!--<script </script </</script>"
-      }
-    },
-    {
-      "data": "<script><!--<script </script </script",
-      "errors": [
-        "(1,8): expected-doctype-but-got-start-tag",
-        "(1,37): expected-named-closing-tag-but-got-eof",
-        "(1,37): unexpected-eof-in-text-mode"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "<!--<script </script </script",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><script><!--<script </script </script</script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script><!--<script </script </script</script>"
-      }
-    },
-    {
-      "data": "<script><!--<script </script </script ",
-      "errors": [
-        "(1,8): expected-doctype-but-got-start-tag",
-        "(1,38): expected-attribute-name-but-got-eof",
-        "(1,38): expected-named-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "<!--<script </script ",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><script><!--<script </script </script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script><!--<script </script </script>"
-      }
-    },
-    {
-      "data": "<script><!--<script </script </script/",
-      "errors": [
-        "(1,8): expected-doctype-but-got-start-tag",
-        "(1,38): unexpected-EOF-after-solidus-in-tag",
-        "(1,38): expected-named-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "<!--<script </script ",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><script><!--<script </script </script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script><!--<script </script </script>"
-      }
-    },
-    {
-      "data": "<script><!--<script </script </script>",
-      "errors": [
-        "(1,8): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "<!--<script </script ",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><script><!--<script </script </script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script><!--<script </script </script>"
-      }
-    },
-    {
-      "data": "<script><!--<script -",
-      "errors": [
-        "(1,8): expected-doctype-but-got-start-tag",
-        "(1,21): eof-in-script-in-script",
-        "(1,21): expected-named-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "<!--<script -",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><script><!--<script -</script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script><!--<script -</script>"
-      }
-    },
-    {
-      "data": "<script><!--<script -a",
-      "errors": [
-        "(1,8): expected-doctype-but-got-start-tag",
-        "(1,22): eof-in-script-in-script",
-        "(1,22): expected-named-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "<!--<script -a",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><script><!--<script -a</script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script><!--<script -a</script>"
-      }
-    },
-    {
-      "data": "<script><!--<script --",
-      "errors": [
-        "(1,8): expected-doctype-but-got-start-tag",
-        "(1,22): eof-in-script-in-script",
-        "(1,22): expected-named-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "<!--<script --",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><script><!--<script --</script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script><!--<script --</script>"
-      }
-    },
-    {
-      "data": "<script><!--<script --a",
-      "errors": [
-        "(1,8): expected-doctype-but-got-start-tag",
-        "(1,23): eof-in-script-in-script",
-        "(1,23): expected-named-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "<!--<script --a",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><script><!--<script --a</script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script><!--<script --a</script>"
-      }
-    },
-    {
-      "data": "<script><!--<script -->",
-      "errors": [
-        "(1,8): expected-doctype-but-got-start-tag",
-        "(1,23): expected-named-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "<!--<script -->",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><script><!--<script --></script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script><!--<script --></script>"
-      }
-    },
-    {
-      "data": "<script><!--<script --><",
-      "errors": [
-        "(1,8): expected-doctype-but-got-start-tag",
-        "(1,24): expected-named-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "<!--<script --><",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><script><!--<script --><</script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script><!--<script --><</script>"
-      }
-    },
-    {
-      "data": "<script><!--<script --></",
-      "errors": [
-        "(1,8): expected-doctype-but-got-start-tag",
-        "(1,25): expected-named-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "<!--<script --></",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><script><!--<script --></</script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script><!--<script --></</script>"
-      }
-    },
-    {
-      "data": "<script><!--<script --></script",
-      "errors": [
-        "(1,8): expected-doctype-but-got-start-tag",
-        "(1,31): expected-named-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "<!--<script --></script",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><script><!--<script --></script</script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script><!--<script --></script</script>"
-      }
-    },
-    {
-      "data": "<script><!--<script --></script ",
-      "errors": [
-        "(1,8): expected-doctype-but-got-start-tag",
-        "(1,32): expected-attribute-name-but-got-eof",
-        "(1,32): expected-named-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "<!--<script -->",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><script><!--<script --></script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script><!--<script --></script>"
-      }
-    },
-    {
-      "data": "<script><!--<script --></script/",
-      "errors": [
-        "(1,8): expected-doctype-but-got-start-tag",
-        "(1,32): unexpected-EOF-after-solidus-in-tag",
-        "(1,32): expected-named-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "<!--<script -->",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><script><!--<script --></script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script><!--<script --></script>"
-      }
-    },
-    {
-      "data": "<script><!--<script --></script>",
-      "errors": [
-        "(1,8): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "<!--<script -->",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><script><!--<script --></script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script><!--<script --></script>"
-      }
-    },
-    {
-      "data": "<script><!--<script><\\/script>--></script>",
-      "errors": [
-        "(1,8): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "<!--<script><\\/script>-->",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><script><!--<script><\\/script>--></script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script><!--<script><\\/script>--></script>"
-      }
-    },
-    {
-      "data": "<script><!--<script></scr'+'ipt>--></script>",
-      "errors": [
-        "(1,8): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "<!--<script></scr'+'ipt>-->",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><script><!--<script></scr'+'ipt>--></script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script><!--<script></scr'+'ipt>--></script>"
-      }
-    },
-    {
-      "data": "<script><!--<script></script><script></script></script>",
-      "errors": [
-        "(1,8): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "<!--<script></script><script></script>",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><script><!--<script></script><script></script></script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script><!--<script></script><script></script></script>"
-      }
-    },
-    {
-      "data": "<script><!--<script></script><script></script>--><!--</script>",
-      "errors": [
-        "(1,8): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "<!--<script></script><script></script>--><!--",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><script><!--<script></script><script></script>--><!--</script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script><!--<script></script><script></script>--><!--</script>"
-      }
-    },
-    {
-      "data": "<script><!--<script></script><script></script>-- ></script>",
-      "errors": [
-        "(1,8): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "<!--<script></script><script></script>-- >",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><script><!--<script></script><script></script>-- ></script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script><!--<script></script><script></script>-- ></script>"
-      }
-    },
-    {
-      "data": "<script><!--<script></script><script></script>- -></script>",
-      "errors": [
-        "(1,8): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "<!--<script></script><script></script>- ->",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><script><!--<script></script><script></script>- -></script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script><!--<script></script><script></script>- -></script>"
-      }
-    },
-    {
-      "data": "<script><!--<script></script><script></script>- - ></script>",
-      "errors": [
-        "(1,8): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "<!--<script></script><script></script>- - >",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><script><!--<script></script><script></script>- - ></script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script><!--<script></script><script></script>- - ></script>"
-      }
-    },
-    {
-      "data": "<script><!--<script></script><script></script>-></script>",
-      "errors": [
-        "(1,8): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "<!--<script></script><script></script>->",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><script><!--<script></script><script></script>-></script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script><!--<script></script><script></script>-></script>"
-      }
-    },
-    {
-      "data": "<script><!--<script>--!></script>X",
-      "errors": [
-        "(1,8): expected-doctype-but-got-start-tag",
-        "(1,34): expected-named-closing-tag-but-got-eof",
-        "(1,34): unexpected-eof-in-text-mode"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "<!--<script>--!></script>X",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><script><!--<script>--!></script>X</script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script><!--<script>--!></script>X</script>"
-      }
-    },
-    {
-      "data": "<script><!--<scr'+'ipt></script>--></script>",
-      "errors": [
-        "(1,8): expected-doctype-but-got-start-tag",
-        "(1,44): unexpected-end-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "no_escape": true,
-          "escaped": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "<!--<scr'+'ipt>",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "-->",
-                    "escaped": true
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><script><!--<scr'+'ipt></script></head><body>--&gt;</body></html>",
-        "noQuirksBodyHtml": "<script><!--<scr'+'ipt></script>--&gt;"
-      }
-    },
-    {
-      "data": "<script><!--<script></scr'+'ipt></script>X",
-      "errors": [
-        "(1,8): expected-doctype-but-got-start-tag",
-        "(1,42): expected-named-closing-tag-but-got-eof",
-        "(1,42): unexpected-eof-in-text-mode"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "<!--<script></scr'+'ipt></script>X",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><script><!--<script></scr'+'ipt></script>X</script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script><!--<script></scr'+'ipt></script>X</script>"
-      }
-    },
-    {
-      "data": "<style><!--<style></style>--></style>",
-      "errors": [
-        "(1,7): expected-doctype-but-got-start-tag",
-        "(1,37): unexpected-end-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "style": true,
-            "body": true
-          },
-          "no_escape": true,
-          "escaped": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "style",
-                    "children": [
-                      {
-                        "text": "<!--<style>",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "-->",
-                    "escaped": true
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><style><!--<style></style></head><body>--&gt;</body></html>",
-        "noQuirksBodyHtml": "<style><!--<style></style>--&gt;"
-      }
-    },
-    {
-      "data": "<style><!--</style>X",
-      "errors": [
-        "(1,7): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "style": true,
-            "body": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "style",
-                    "children": [
-                      {
-                        "text": "<!--",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "X"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><style><!--</style></head><body>X</body></html>",
-        "noQuirksBodyHtml": "<style><!--</style>X"
-      }
-    },
-    {
-      "data": "<style><!--...</style>...--></style>",
-      "errors": [
-        "(1,7): expected-doctype-but-got-start-tag",
-        "(1,36): unexpected-end-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "style": true,
-            "body": true
-          },
-          "no_escape": true,
-          "escaped": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "style",
-                    "children": [
-                      {
-                        "text": "<!--...",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "...-->",
-                    "escaped": true
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><style><!--...</style></head><body>...--&gt;</body></html>",
-        "noQuirksBodyHtml": "<style><!--...</style>...--&gt;"
-      }
-    },
-    {
-      "data": "<style><!--<br><html xmlns:v=\"urn:schemas-microsoft-com:vml\"><!--[if !mso]><style></style>X",
-      "errors": [
-        "(1,7): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "style": true,
-            "body": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "style",
-                    "children": [
-                      {
-                        "text": "<!--<br><html xmlns:v=\"urn:schemas-microsoft-com:vml\"><!--[if !mso]><style>",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "X"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><style><!--<br><html xmlns:v=\"urn:schemas-microsoft-com:vml\"><!--[if !mso]><style></style></head><body>X</body></html>",
-        "noQuirksBodyHtml": "<style><!--<br><html xmlns:v=\"urn:schemas-microsoft-com:vml\"><!--[if !mso]><style></style>X"
-      }
-    },
-    {
-      "data": "<style><!--...<style><!--...--!></style>--></style>",
-      "errors": [
-        "(1,7): expected-doctype-but-got-start-tag",
-        "(1,51): unexpected-end-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "style": true,
-            "body": true
-          },
-          "no_escape": true,
-          "escaped": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "style",
-                    "children": [
-                      {
-                        "text": "<!--...<style><!--...--!>",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "-->",
-                    "escaped": true
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><style><!--...<style><!--...--!></style></head><body>--&gt;</body></html>",
-        "noQuirksBodyHtml": "<style><!--...<style><!--...--!></style>--&gt;"
-      }
-    },
-    {
-      "data": "<style><!--...</style><!-- --><style>@import ...</style>",
-      "errors": [
-        "(1,7): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "style": true,
-            "body": true
-          },
-          "no_escape": true,
-          "comment": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "style",
-                    "children": [
-                      {
-                        "text": "<!--...",
-                        "no_escape": true
-                      }
-                    ]
-                  },
-                  {
-                    "comment": " "
-                  },
-                  {
-                    "tag": "style",
-                    "children": [
-                      {
-                        "text": "@import ...",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><style><!--...</style><!-- --><style>@import ...</style></head><body></body></html>",
-        "noQuirksBodyHtml": "<style><!--...</style><!-- --><style>@import ...</style>"
-      }
-    },
-    {
-      "data": "<style>...<style><!--...</style><!-- --></style>",
-      "errors": [
-        "(1,7): expected-doctype-but-got-start-tag",
-        "(1,48): unexpected-end-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "style": true,
-            "body": true
-          },
-          "no_escape": true,
-          "comment": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "style",
-                    "children": [
-                      {
-                        "text": "...<style><!--...",
-                        "no_escape": true
-                      }
-                    ]
-                  },
-                  {
-                    "comment": " "
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><style>...<style><!--...</style><!-- --></head><body></body></html>",
-        "noQuirksBodyHtml": "<style>...<style><!--...</style><!-- -->"
-      }
-    },
-    {
-      "data": "<style>...<!--[if IE]><style>...</style>X",
-      "errors": [
-        "(1,7): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "style": true,
-            "body": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "style",
-                    "children": [
-                      {
-                        "text": "...<!--[if IE]><style>...",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "X"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><style>...<!--[if IE]><style>...</style></head><body>X</body></html>",
-        "noQuirksBodyHtml": "<style>...<!--[if IE]><style>...</style>X"
-      }
-    },
-    {
-      "data": "<title><!--<title></title>--></title>",
-      "errors": [
-        "(1,7): expected-doctype-but-got-start-tag",
-        "(1,37): unexpected-end-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "title": true,
-            "body": true
-          },
-          "escaped": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "title",
-                    "children": [
-                      {
-                        "text": "<!--<title>",
-                        "escaped": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "-->",
-                    "escaped": true
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><title>&lt;!--&lt;title&gt;</title></head><body>--&gt;</body></html>",
-        "noQuirksBodyHtml": "<title>&lt;!--&lt;title&gt;</title>--&gt;"
-      }
-    },
-    {
-      "data": "<title>&lt;/title></title>",
-      "errors": [
-        "(1,7): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "title": true,
-            "body": true
-          },
-          "escaped": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "title",
-                    "children": [
-                      {
-                        "text": "</title>",
-                        "escaped": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><title>&lt;/title&gt;</title></head><body></body></html>",
-        "noQuirksBodyHtml": "<title>&lt;/title&gt;</title>"
-      }
-    },
-    {
-      "data": "<title>foo/title><link></head><body>X",
-      "errors": [
-        "(1,7): expected-doctype-but-got-start-tag",
-        "(1,37): expected-named-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "title": true,
-            "body": true
-          },
-          "escaped": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "title",
-                    "children": [
-                      {
-                        "text": "foo/title><link></head><body>X",
-                        "escaped": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><title>foo/title&gt;&lt;link&gt;&lt;/head&gt;&lt;body&gt;X</title></head><body></body></html>",
-        "noQuirksBodyHtml": "<title>foo/title&gt;&lt;link&gt;&lt;/head&gt;&lt;body&gt;X</title>"
-      }
-    },
-    {
-      "data": "<noscript><!--<noscript></noscript>--></noscript>",
-      "errors": [
-        "(1,10): expected-doctype-but-got-start-tag",
-        "(1,49): unexpected-end-tag"
-      ],
-      "script": "on",
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "noscript": true,
-            "body": true
-          },
-          "no_escape": true,
-          "escaped": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "noscript",
-                    "children": [
-                      {
-                        "text": "<!--<noscript>",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "-->",
-                    "escaped": true
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><noscript><!--<noscript></noscript></head><body>--&gt;</body></html>",
-        "noQuirksBodyHtml": "<noscript><!--<noscript></noscript>--></noscript>"
-      }
-    },
-    {
-      "data": "<noscript><!--<noscript></noscript>--></noscript>",
-      "errors": [
-        " * (1,11) missing DOCTYPE"
-      ],
-      "script": "off",
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "noscript": true,
-            "body": true
-          },
-          "comment": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "noscript",
-                    "children": [
-                      {
-                        "comment": "<noscript></noscript>"
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><noscript><!--<noscript></noscript>--></noscript></head><body></body></html>",
-        "noQuirksBodyHtml": "<noscript><!--<noscript></noscript>--></noscript>"
-      }
-    },
-    {
-      "data": "<noscript><!--</noscript>X<noscript>--></noscript>",
-      "errors": [
-        "(1,10): expected-doctype-but-got-start-tag"
-      ],
-      "script": "on",
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "noscript": true,
-            "body": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "noscript",
-                    "children": [
-                      {
-                        "text": "<!--",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "X"
-                  },
-                  {
-                    "tag": "noscript",
-                    "children": [
-                      {
-                        "text": "-->",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><noscript><!--</noscript></head><body>X<noscript>--></noscript></body></html>",
-        "noQuirksBodyHtml": "<noscript><!--</noscript>X<noscript>--></noscript>"
-      }
-    },
-    {
-      "data": "<noscript><!--</noscript>X<noscript>--></noscript>",
-      "errors": [
-        "(1,10): expected-doctype-but-got-start-tag"
-      ],
-      "script": "off",
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "noscript": true,
-            "body": true
-          },
-          "comment": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "noscript",
-                    "children": [
-                      {
-                        "comment": "</noscript>X<noscript>"
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><noscript><!--</noscript>X<noscript>--></noscript></head><body></body></html>",
-        "noQuirksBodyHtml": "<noscript><!--</noscript>X<noscript>--></noscript>"
-      }
-    },
-    {
-      "data": "<noscript><iframe></noscript>X",
-      "errors": [
-        "(1,10): expected-doctype-but-got-start-tag"
-      ],
-      "script": "on",
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "noscript": true,
-            "body": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "noscript",
-                    "children": [
-                      {
-                        "text": "<iframe>",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "X"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><noscript><iframe></noscript></head><body>X</body></html>",
-        "noQuirksBodyHtml": "<noscript><iframe></noscript>X</iframe></noscript>"
-      }
-    },
-    {
-      "data": "<noscript><iframe></noscript>X",
-      "errors": [
-        " * (1,11) missing DOCTYPE",
-        " * (1,19) unexpected token in head noscript",
-        " * (1,31) unexpected EOF"
-      ],
-      "script": "off",
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "noscript": true,
-            "body": true,
-            "iframe": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "noscript"
-                  }
-                ]
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "iframe",
-                    "children": [
-                      {
-                        "text": "</noscript>X",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><noscript></noscript></head><body><iframe></noscript>X</iframe></body></html>",
-        "noQuirksBodyHtml": "<noscript><iframe></noscript>X</iframe></noscript>"
-      }
-    },
-    {
-      "data": "<noframes><!--<noframes></noframes>--></noframes>",
-      "errors": [
-        "(1,10): expected-doctype-but-got-start-tag",
-        "(1,49): unexpected-end-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "noframes": true,
-            "body": true
-          },
-          "no_escape": true,
-          "escaped": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "noframes",
-                    "children": [
-                      {
-                        "text": "<!--<noframes>",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "-->",
-                    "escaped": true
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><noframes><!--<noframes></noframes></head><body>--&gt;</body></html>",
-        "noQuirksBodyHtml": "<noframes><!--<noframes></noframes>--&gt;"
-      }
-    },
-    {
-      "data": "<noframes><body><script><!--...</script></body></noframes></html>",
-      "errors": [
-        "(1,10): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "noframes": true,
-            "body": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "noframes",
-                    "children": [
-                      {
-                        "text": "<body><script><!--...</script></body>",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><noframes><body><script><!--...</script></body></noframes></head><body></body></html>",
-        "noQuirksBodyHtml": "<noframes><body><script><!--...</script></body></noframes>"
-      }
-    },
-    {
-      "data": "<textarea><!--<textarea></textarea>--></textarea>",
-      "errors": [
-        "(1,10): expected-doctype-but-got-start-tag",
-        "(1,49): unexpected-end-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "textarea": true
-          },
-          "escaped": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "textarea",
-                    "children": [
-                      {
-                        "text": "<!--<textarea>",
-                        "escaped": true
-                      }
-                    ]
-                  },
-                  {
-                    "text": "-->",
-                    "escaped": true
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><textarea>&lt;!--&lt;textarea&gt;</textarea>--&gt;</body></html>",
-        "noQuirksBodyHtml": "<textarea>&lt;!--&lt;textarea&gt;</textarea>--&gt;"
-      }
-    },
-    {
-      "data": "<textarea>&lt;/textarea></textarea>",
-      "errors": [
-        "(1,10): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "textarea": true
-          },
-          "escaped": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "textarea",
-                    "children": [
-                      {
-                        "text": "</textarea>",
-                        "escaped": true
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><textarea>&lt;/textarea&gt;</textarea></body></html>",
-        "noQuirksBodyHtml": "<textarea>&lt;/textarea&gt;</textarea>"
-      }
-    },
-    {
-      "data": "<iframe><!--<iframe></iframe>--></iframe>",
-      "errors": [
-        "(1,8): expected-doctype-but-got-start-tag",
-        "(1,41): unexpected-end-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "iframe": true
-          },
-          "no_escape": true,
-          "escaped": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "iframe",
-                    "children": [
-                      {
-                        "text": "<!--<iframe>",
-                        "no_escape": true
-                      }
-                    ]
-                  },
-                  {
-                    "text": "-->",
-                    "escaped": true
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><iframe><!--<iframe></iframe>--&gt;</body></html>",
-        "noQuirksBodyHtml": "<iframe><!--<iframe></iframe>--&gt;"
-      }
-    },
-    {
-      "data": "<iframe>...<!--X->...<!--/X->...</iframe>",
-      "errors": [
-        "(1,8): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "iframe": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "iframe",
-                    "children": [
-                      {
-                        "text": "...<!--X->...<!--/X->...",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><iframe>...<!--X->...<!--/X->...</iframe></body></html>",
-        "noQuirksBodyHtml": "<iframe>...<!--X->...<!--/X->...</iframe>"
-      }
-    },
-    {
-      "data": "<xmp><!--<xmp></xmp>--></xmp>",
-      "errors": [
-        "(1,5): expected-doctype-but-got-start-tag",
-        "(1,29): unexpected-end-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "xmp": true
-          },
-          "no_escape": true,
-          "escaped": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "xmp",
-                    "children": [
-                      {
-                        "text": "<!--<xmp>",
-                        "no_escape": true
-                      }
-                    ]
-                  },
-                  {
-                    "text": "-->",
-                    "escaped": true
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><xmp><!--<xmp></xmp>--&gt;</body></html>",
-        "noQuirksBodyHtml": "<xmp><!--<xmp></xmp>--&gt;"
-      }
-    },
-    {
-      "data": "<noembed><!--<noembed></noembed>--></noembed>",
-      "errors": [
-        "(1,9): expected-doctype-but-got-start-tag",
-        "(1,45): unexpected-end-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "noembed": true
-          },
-          "no_escape": true,
-          "escaped": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "noembed",
-                    "children": [
-                      {
-                        "text": "<!--<noembed>",
-                        "no_escape": true
-                      }
-                    ]
-                  },
-                  {
-                    "text": "-->",
-                    "escaped": true
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><noembed><!--<noembed></noembed>--&gt;</body></html>",
-        "noQuirksBodyHtml": "<noembed><!--<noembed></noembed>--&gt;"
-      }
-    },
-    {
-      "data": "<!doctype html><table>\n",
-      "errors": [
-        "(2,0): eof-in-table"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "text": "\n"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><table>\n</table></body></html>",
-        "noQuirksBodyHtml": "<table>\n</table>"
-      }
-    },
-    {
-      "data": "<!doctype html><table><td><span><font></span><span>",
-      "errors": [
-        "(1,26): unexpected-cell-in-table-body",
-        "(1,45): unexpected-end-tag",
-        "(1,51): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true,
-            "tbody": true,
-            "tr": true,
-            "td": true,
-            "span": true,
-            "font": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "tbody",
-                        "children": [
-                          {
-                            "tag": "tr",
-                            "children": [
-                              {
-                                "tag": "td",
-                                "children": [
-                                  {
-                                    "tag": "span",
-                                    "children": [
-                                      {
-                                        "tag": "font"
-                                      }
-                                    ]
-                                  },
-                                  {
-                                    "tag": "font",
-                                    "children": [
-                                      {
-                                        "tag": "span"
-                                      }
-                                    ]
-                                  }
-                                ]
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><table><tbody><tr><td><span><font></font></span><font><span></span></font></td></tr></tbody></table></body></html>",
-        "noQuirksBodyHtml": "<table><tbody><tr><td><span><font></font></span><font><span></span></font></td></tr></tbody></table>"
-      }
-    },
-    {
-      "data": "<!doctype html><form><table></form><form></table></form>",
-      "errors": [
-        "(1,35): unexpected-end-tag-implies-table-voodoo",
-        "(1,35): unexpected-end-tag",
-        "(1,41): unexpected-form-in-table",
-        "(1,56): unexpected-end-tag",
-        "(1,56): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "form": true,
-            "table": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "form",
-                    "children": [
-                      {
-                        "tag": "table",
-                        "children": [
-                          {
-                            "tag": "form"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><form><table><form></form></table></form></body></html>",
-        "noQuirksBodyHtml": "<form><table><form></form></table></form>"
-      }
-    }
-  ],
-  "tests17.dat": [
-    {
-      "data": "<!doctype html><table><tbody><select><tr>",
-      "errors": [
-        "(1,37): unexpected-start-tag-implies-table-voodoo",
-        "(1,41): unexpected-table-element-start-tag-in-select-in-table",
-        "(1,41): eof-in-table"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "select": true,
-            "table": true,
-            "tbody": true,
-            "tr": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "select"
-                  },
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "tbody",
-                        "children": [
-                          {
-                            "tag": "tr"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><select></select><table><tbody><tr></tr></tbody></table></body></html>",
-        "noQuirksBodyHtml": "<select></select><table><tbody><tr></tr></tbody></table>"
-      }
-    },
-    {
-      "data": "<!doctype html><table><tr><select><td>",
-      "errors": [
-        "(1,34): unexpected-start-tag-implies-table-voodoo",
-        "(1,38): unexpected-table-element-start-tag-in-select-in-table",
-        "(1,38): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "select": true,
-            "table": true,
-            "tbody": true,
-            "tr": true,
-            "td": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "select"
-                  },
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "tbody",
-                        "children": [
-                          {
-                            "tag": "tr",
-                            "children": [
-                              {
-                                "tag": "td"
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><select></select><table><tbody><tr><td></td></tr></tbody></table></body></html>",
-        "noQuirksBodyHtml": "<select></select><table><tbody><tr><td></td></tr></tbody></table>"
-      }
-    },
-    {
-      "data": "<!doctype html><table><tr><td><select><td>",
-      "errors": [
-        "(1,42): unexpected-table-element-start-tag-in-select-in-table",
-        "(1,42): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true,
-            "tbody": true,
-            "tr": true,
-            "td": true,
-            "select": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "tbody",
-                        "children": [
-                          {
-                            "tag": "tr",
-                            "children": [
-                              {
-                                "tag": "td",
-                                "children": [
-                                  {
-                                    "tag": "select"
-                                  }
-                                ]
-                              },
-                              {
-                                "tag": "td"
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><table><tbody><tr><td><select></select></td><td></td></tr></tbody></table></body></html>",
-        "noQuirksBodyHtml": "<table><tbody><tr><td><select></select></td><td></td></tr></tbody></table>"
-      }
-    },
-    {
-      "data": "<!doctype html><table><tr><th><select><td>",
-      "errors": [
-        "(1,42): unexpected-table-element-start-tag-in-select-in-table",
-        "(1,42): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true,
-            "tbody": true,
-            "tr": true,
-            "th": true,
-            "select": true,
-            "td": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "tbody",
-                        "children": [
-                          {
-                            "tag": "tr",
-                            "children": [
-                              {
-                                "tag": "th",
-                                "children": [
-                                  {
-                                    "tag": "select"
-                                  }
-                                ]
-                              },
-                              {
-                                "tag": "td"
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><table><tbody><tr><th><select></select></th><td></td></tr></tbody></table></body></html>",
-        "noQuirksBodyHtml": "<table><tbody><tr><th><select></select></th><td></td></tr></tbody></table>"
-      }
-    },
-    {
-      "data": "<!doctype html><table><caption><select><tr>",
-      "errors": [
-        "(1,43): unexpected-table-element-start-tag-in-select-in-table",
-        "(1,43): eof-in-table"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true,
-            "caption": true,
-            "select": true,
-            "tbody": true,
-            "tr": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "caption",
-                        "children": [
-                          {
-                            "tag": "select"
-                          }
-                        ]
-                      },
-                      {
-                        "tag": "tbody",
-                        "children": [
-                          {
-                            "tag": "tr"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><table><caption><select></select></caption><tbody><tr></tr></tbody></table></body></html>",
-        "noQuirksBodyHtml": "<table><caption><select></select></caption><tbody><tr></tr></tbody></table>"
-      }
-    },
-    {
-      "data": "<!doctype html><select><tr>",
-      "errors": [
-        "(1,27): unexpected-start-tag-in-select",
-        "(1,27): eof-in-select"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "select": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "select"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><select></select></body></html>",
-        "noQuirksBodyHtml": "<select></select>"
-      }
-    },
-    {
-      "data": "<!doctype html><select><td>",
-      "errors": [
-        "(1,27): unexpected-start-tag-in-select",
-        "(1,27): eof-in-select"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "select": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "select"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><select></select></body></html>",
-        "noQuirksBodyHtml": "<select></select>"
-      }
-    },
-    {
-      "data": "<!doctype html><select><th>",
-      "errors": [
-        "(1,27): unexpected-start-tag-in-select",
-        "(1,27): eof-in-select"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "select": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "select"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><select></select></body></html>",
-        "noQuirksBodyHtml": "<select></select>"
-      }
-    },
-    {
-      "data": "<!doctype html><select><tbody>",
-      "errors": [
-        "(1,30): unexpected-start-tag-in-select",
-        "(1,30): eof-in-select"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "select": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "select"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><select></select></body></html>",
-        "noQuirksBodyHtml": "<select></select>"
-      }
-    },
-    {
-      "data": "<!doctype html><select><thead>",
-      "errors": [
-        "(1,30): unexpected-start-tag-in-select",
-        "(1,30): eof-in-select"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "select": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "select"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><select></select></body></html>",
-        "noQuirksBodyHtml": "<select></select>"
-      }
-    },
-    {
-      "data": "<!doctype html><select><tfoot>",
-      "errors": [
-        "(1,30): unexpected-start-tag-in-select",
-        "(1,30): eof-in-select"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "select": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "select"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><select></select></body></html>",
-        "noQuirksBodyHtml": "<select></select>"
-      }
-    },
-    {
-      "data": "<!doctype html><select><caption>",
-      "errors": [
-        "(1,32): unexpected-start-tag-in-select",
-        "(1,32): eof-in-select"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "select": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "select"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><select></select></body></html>",
-        "noQuirksBodyHtml": "<select></select>"
-      }
-    },
-    {
-      "data": "<!doctype html><table><tr></table>a",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true,
-            "tbody": true,
-            "tr": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "tbody",
-                        "children": [
-                          {
-                            "tag": "tr"
-                          }
-                        ]
-                      }
-                    ]
-                  },
-                  {
-                    "text": "a"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><table><tbody><tr></tr></tbody></table>a</body></html>",
-        "noQuirksBodyHtml": "<table><tbody><tr></tr></tbody></table>a"
-      }
-    }
-  ],
-  "tests18.dat": [
-    {
-      "data": "<!doctype html><plaintext></plaintext>",
-      "errors": [
-        "(1,38): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "plaintext": true
-          },
-          "doctype": true,
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "plaintext",
-                    "children": [
-                      {
-                        "text": "</plaintext>",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><plaintext></plaintext></plaintext></body></html>",
-        "noQuirksBodyHtml": "<plaintext></plaintext></plaintext>"
-      }
-    },
-    {
-      "data": "<!doctype html><table><plaintext></plaintext>",
-      "errors": [
-        "(1,33): foster-parenting-start-tag",
-        "(1,34): foster-parenting-character",
-        "(1,35): foster-parenting-character",
-        "(1,36): foster-parenting-character",
-        "(1,37): foster-parenting-character",
-        "(1,38): foster-parenting-character",
-        "(1,39): foster-parenting-character",
-        "(1,40): foster-parenting-character",
-        "(1,41): foster-parenting-character",
-        "(1,42): foster-parenting-character",
-        "(1,43): foster-parenting-character",
-        "(1,44): foster-parenting-character",
-        "(1,45): foster-parenting-character",
-        "(1,45): eof-in-table"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "plaintext": true,
-            "table": true
-          },
-          "doctype": true,
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "plaintext",
-                    "children": [
-                      {
-                        "text": "</plaintext>",
-                        "no_escape": true
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "table"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><plaintext></plaintext></plaintext><table></table></body></html>",
-        "noQuirksBodyHtml": "<plaintext></plaintext></plaintext><table></table>"
-      }
-    },
-    {
-      "data": "<!doctype html><table><tbody><plaintext></plaintext>",
-      "errors": [
-        "(1,40): foster-parenting-start-tag",
-        "(1,41): foster-parenting-character",
-        "(1,41): foster-parenting-character",
-        "(1,41): foster-parenting-character",
-        "(1,41): foster-parenting-character",
-        "(1,41): foster-parenting-character",
-        "(1,41): foster-parenting-character",
-        "(1,41): foster-parenting-character",
-        "(1,41): foster-parenting-character",
-        "(1,41): foster-parenting-character",
-        "(1,41): foster-parenting-character",
-        "(1,41): foster-parenting-character",
-        "(1,41): foster-parenting-character",
-        "(1,52): eof-in-table"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "plaintext": true,
-            "table": true,
-            "tbody": true
-          },
-          "doctype": true,
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "plaintext",
-                    "children": [
-                      {
-                        "text": "</plaintext>",
-                        "no_escape": true
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "tbody"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><plaintext></plaintext></plaintext><table><tbody></tbody></table></body></html>",
-        "noQuirksBodyHtml": "<plaintext></plaintext></plaintext><table><tbody></tbody></table>"
-      }
-    },
-    {
-      "data": "<!doctype html><table><tbody><tr><plaintext></plaintext>",
-      "errors": [
-        "(1,44): foster-parenting-start-tag",
-        "(1,45): foster-parenting-character",
-        "(1,46): foster-parenting-character",
-        "(1,47): foster-parenting-character",
-        "(1,48): foster-parenting-character",
-        "(1,49): foster-parenting-character",
-        "(1,50): foster-parenting-character",
-        "(1,51): foster-parenting-character",
-        "(1,52): foster-parenting-character",
-        "(1,53): foster-parenting-character",
-        "(1,54): foster-parenting-character",
-        "(1,55): foster-parenting-character",
-        "(1,56): foster-parenting-character",
-        "(1,56): eof-in-table"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "plaintext": true,
-            "table": true,
-            "tbody": true,
-            "tr": true
-          },
-          "doctype": true,
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "plaintext",
-                    "children": [
-                      {
-                        "text": "</plaintext>",
-                        "no_escape": true
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "tbody",
-                        "children": [
-                          {
-                            "tag": "tr"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><plaintext></plaintext></plaintext><table><tbody><tr></tr></tbody></table></body></html>",
-        "noQuirksBodyHtml": "<plaintext></plaintext></plaintext><table><tbody><tr></tr></tbody></table>"
-      }
-    },
-    {
-      "data": "<!doctype html><table><td><plaintext></plaintext>",
-      "errors": [
-        "(1,26): unexpected-cell-in-table-body",
-        "(1,49): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true,
-            "tbody": true,
-            "tr": true,
-            "td": true,
-            "plaintext": true
-          },
-          "doctype": true,
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "tbody",
-                        "children": [
-                          {
-                            "tag": "tr",
-                            "children": [
-                              {
-                                "tag": "td",
-                                "children": [
-                                  {
-                                    "tag": "plaintext",
-                                    "children": [
-                                      {
-                                        "text": "</plaintext>",
-                                        "no_escape": true
-                                      }
-                                    ]
-                                  }
-                                ]
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><table><tbody><tr><td><plaintext></plaintext></plaintext></td></tr></tbody></table></body></html>",
-        "noQuirksBodyHtml": "<table><tbody><tr><td><plaintext></plaintext></plaintext></td></tr></tbody></table>"
-      }
-    },
-    {
-      "data": "<!doctype html><table><caption><plaintext></plaintext>",
-      "errors": [
-        "(1,54): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true,
-            "caption": true,
-            "plaintext": true
-          },
-          "doctype": true,
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "caption",
-                        "children": [
-                          {
-                            "tag": "plaintext",
-                            "children": [
-                              {
-                                "text": "</plaintext>",
-                                "no_escape": true
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><table><caption><plaintext></plaintext></plaintext></caption></table></body></html>",
-        "noQuirksBodyHtml": "<table><caption><plaintext></plaintext></plaintext></caption></table>"
-      }
-    },
-    {
-      "data": "<!doctype html><table><tr><style></script></style>abc",
-      "errors": [
-        "(1,51): foster-parenting-character",
-        "(1,52): foster-parenting-character",
-        "(1,53): foster-parenting-character",
-        "(1,53): eof-in-table"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true,
-            "tbody": true,
-            "tr": true,
-            "style": true
-          },
-          "doctype": true,
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "abc"
-                  },
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "tbody",
-                        "children": [
-                          {
-                            "tag": "tr",
-                            "children": [
-                              {
-                                "tag": "style",
-                                "children": [
-                                  {
-                                    "text": "</script>",
-                                    "no_escape": true
-                                  }
-                                ]
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body>abc<table><tbody><tr><style></script></style></tr></tbody></table></body></html>",
-        "noQuirksBodyHtml": "abc<table><tbody><tr><style></script></style></tr></tbody></table>"
-      }
-    },
-    {
-      "data": "<!doctype html><table><tr><script></style></script>abc",
-      "errors": [
-        "(1,52): foster-parenting-character",
-        "(1,53): foster-parenting-character",
-        "(1,54): foster-parenting-character",
-        "(1,54): eof-in-table"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true,
-            "tbody": true,
-            "tr": true,
-            "script": true
-          },
-          "doctype": true,
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "abc"
-                  },
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "tbody",
-                        "children": [
-                          {
-                            "tag": "tr",
-                            "children": [
-                              {
-                                "tag": "script",
-                                "children": [
-                                  {
-                                    "text": "</style>",
-                                    "no_escape": true
-                                  }
-                                ]
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body>abc<table><tbody><tr><script></style></script></tr></tbody></table></body></html>",
-        "noQuirksBodyHtml": "abc<table><tbody><tr><script></style></script></tr></tbody></table>"
-      }
-    },
-    {
-      "data": "<!doctype html><table><caption><style></script></style>abc",
-      "errors": [
-        "(1,58): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true,
-            "caption": true,
-            "style": true
-          },
-          "doctype": true,
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "caption",
-                        "children": [
-                          {
-                            "tag": "style",
-                            "children": [
-                              {
-                                "text": "</script>",
-                                "no_escape": true
-                              }
-                            ]
-                          },
-                          {
-                            "text": "abc"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><table><caption><style></script></style>abc</caption></table></body></html>",
-        "noQuirksBodyHtml": "<table><caption><style></script></style>abc</caption></table>"
-      }
-    },
-    {
-      "data": "<!doctype html><table><td><style></script></style>abc",
-      "errors": [
-        "(1,26): unexpected-cell-in-table-body",
-        "(1,53): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true,
-            "tbody": true,
-            "tr": true,
-            "td": true,
-            "style": true
-          },
-          "doctype": true,
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "tbody",
-                        "children": [
-                          {
-                            "tag": "tr",
-                            "children": [
-                              {
-                                "tag": "td",
-                                "children": [
-                                  {
-                                    "tag": "style",
-                                    "children": [
-                                      {
-                                        "text": "</script>",
-                                        "no_escape": true
-                                      }
-                                    ]
-                                  },
-                                  {
-                                    "text": "abc"
-                                  }
-                                ]
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><table><tbody><tr><td><style></script></style>abc</td></tr></tbody></table></body></html>",
-        "noQuirksBodyHtml": "<table><tbody><tr><td><style></script></style>abc</td></tr></tbody></table>"
-      }
-    },
-    {
-      "data": "<!doctype html><select><script></style></script>abc",
-      "errors": [
-        "(1,51): eof-in-select"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "select": true,
-            "script": true
-          },
-          "doctype": true,
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "select",
-                    "children": [
-                      {
-                        "tag": "script",
-                        "children": [
-                          {
-                            "text": "</style>",
-                            "no_escape": true
-                          }
-                        ]
-                      },
-                      {
-                        "text": "abc"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><select><script></style></script>abc</select></body></html>",
-        "noQuirksBodyHtml": "<select><script></style></script>abc</select>"
-      }
-    },
-    {
-      "data": "<!doctype html><table><select><script></style></script>abc",
-      "errors": [
-        "(1,30): unexpected-start-tag-implies-table-voodoo",
-        "(1,58): eof-in-select"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "select": true,
-            "script": true,
-            "table": true
-          },
-          "doctype": true,
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "select",
-                    "children": [
-                      {
-                        "tag": "script",
-                        "children": [
-                          {
-                            "text": "</style>",
-                            "no_escape": true
-                          }
-                        ]
-                      },
-                      {
-                        "text": "abc"
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "table"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><select><script></style></script>abc</select><table></table></body></html>",
-        "noQuirksBodyHtml": "<select><script></style></script>abc</select><table></table>"
-      }
-    },
-    {
-      "data": "<!doctype html><table><tr><select><script></style></script>abc",
-      "errors": [
-        "(1,34): unexpected-start-tag-implies-table-voodoo",
-        "(1,62): eof-in-select"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "select": true,
-            "script": true,
-            "table": true,
-            "tbody": true,
-            "tr": true
-          },
-          "doctype": true,
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "select",
-                    "children": [
-                      {
-                        "tag": "script",
-                        "children": [
-                          {
-                            "text": "</style>",
-                            "no_escape": true
-                          }
-                        ]
-                      },
-                      {
-                        "text": "abc"
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "tbody",
-                        "children": [
-                          {
-                            "tag": "tr"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><select><script></style></script>abc</select><table><tbody><tr></tr></tbody></table></body></html>",
-        "noQuirksBodyHtml": "<select><script></style></script>abc</select><table><tbody><tr></tr></tbody></table>"
-      }
-    },
-    {
-      "data": "<!doctype html><frameset></frameset><noframes>abc",
-      "errors": [
-        "(1,49): expected-named-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "frameset": true,
-            "noframes": true
-          },
-          "doctype": true,
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "frameset"
-              },
-              {
-                "tag": "noframes",
-                "children": [
-                  {
-                    "text": "abc",
-                    "no_escape": true
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><frameset></frameset><noframes>abc</noframes></html>",
-        "noQuirksBodyHtml": "<noframes>abc</noframes>"
-      }
-    },
-    {
-      "data": "<!doctype html><frameset></frameset><noframes>abc</noframes><!--abc-->",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "frameset": true,
-            "noframes": true
-          },
-          "doctype": true,
-          "no_escape": true,
-          "comment": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "frameset"
-              },
-              {
-                "tag": "noframes",
-                "children": [
-                  {
-                    "text": "abc",
-                    "no_escape": true
-                  }
-                ]
-              },
-              {
-                "comment": "abc"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><frameset></frameset><noframes>abc</noframes><!--abc--></html>",
-        "noQuirksBodyHtml": "<noframes>abc</noframes><!--abc-->"
-      }
-    },
-    {
-      "data": "<!doctype html><frameset></frameset></html><noframes>abc",
-      "errors": [
-        "(1,56): expected-named-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "frameset": true,
-            "noframes": true
-          },
-          "doctype": true,
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "frameset"
-              },
-              {
-                "tag": "noframes",
-                "children": [
-                  {
-                    "text": "abc",
-                    "no_escape": true
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><frameset></frameset><noframes>abc</noframes></html>",
-        "noQuirksBodyHtml": "<noframes>abc</noframes>"
-      }
-    },
-    {
-      "data": "<!doctype html><frameset></frameset></html><noframes>abc</noframes><!--abc-->",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "frameset": true,
-            "noframes": true
-          },
-          "doctype": true,
-          "no_escape": true,
-          "comment": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "frameset"
-              },
-              {
-                "tag": "noframes",
-                "children": [
-                  {
-                    "text": "abc",
-                    "no_escape": true
-                  }
-                ]
-              }
-            ]
-          },
-          {
-            "comment": "abc"
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><frameset></frameset><noframes>abc</noframes></html><!--abc-->",
-        "noQuirksBodyHtml": "<noframes>abc</noframes><!--abc-->"
-      }
-    },
-    {
-      "data": "<!doctype html><table><tr></tbody><tfoot>",
-      "errors": [
-        "(1,41): eof-in-table"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true,
-            "tbody": true,
-            "tr": true,
-            "tfoot": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "tbody",
-                        "children": [
-                          {
-                            "tag": "tr"
-                          }
-                        ]
-                      },
-                      {
-                        "tag": "tfoot"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><table><tbody><tr></tr></tbody><tfoot></tfoot></table></body></html>",
-        "noQuirksBodyHtml": "<table><tbody><tr></tr></tbody><tfoot></tfoot></table>"
-      }
-    },
-    {
-      "data": "<!doctype html><table><td><svg></svg>abc<td>",
-      "errors": [
-        "(1,26): unexpected-cell-in-table-body",
-        "(1,44): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true,
-            "tbody": true,
-            "tr": true,
-            "td": true,
-            "svg svg": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "tbody",
-                        "children": [
-                          {
-                            "tag": "tr",
-                            "children": [
-                              {
-                                "tag": "td",
-                                "children": [
-                                  {
-                                    "tag": "svg",
-                                    "ns": "http://www.w3.org/2000/svg"
-                                  },
-                                  {
-                                    "text": "abc"
-                                  }
-                                ]
-                              },
-                              {
-                                "tag": "td"
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><table><tbody><tr><td><svg></svg>abc</td><td></td></tr></tbody></table></body></html>",
-        "noQuirksBodyHtml": "<table><tbody><tr><td><svg></svg>abc</td><td></td></tr></tbody></table>"
-      }
-    }
-  ],
-  "tests19.dat": [
-    {
-      "data": "<!doctype html><math><mn DefinitionUrl=\"foo\">",
-      "errors": [
-        "(1,45): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "math math": true,
-            "math mn": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "math",
-                    "ns": "http://www.w3.org/1998/Math/MathML",
-                    "children": [
-                      {
-                        "tag": "mn",
-                        "ns": "http://www.w3.org/1998/Math/MathML",
-                        "attrs": [
-                          {
-                            "name": "definitionURL",
-                            "value": "foo"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><math><mn definitionURL=\"foo\"></mn></math></body></html>",
-        "noQuirksBodyHtml": "<math><mn definitionURL=\"foo\"></mn></math>"
-      }
-    },
-    {
-      "data": "<!doctype html><html></p><!--foo-->",
-      "errors": [
-        "(1,25): end-tag-after-implied-root"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "doctype": true,
-          "comment": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "comment": "foo"
-              },
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><!--foo--><head></head><body></body></html>",
-        "noQuirksBodyHtml": "<p></p><!--foo-->"
-      }
-    },
-    {
-      "data": "<!doctype html><head></head></p><!--foo-->",
-      "errors": [
-        "(1,32): unexpected-end-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "doctype": true,
-          "comment": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "comment": "foo"
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><!--foo--><body></body></html>",
-        "noQuirksBodyHtml": "<p></p><!--foo-->"
-      }
-    },
-    {
-      "data": "<!doctype html><body><p><pre>",
-      "errors": [
-        "(1,29): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "p": true,
-            "pre": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "p"
-                  },
-                  {
-                    "tag": "pre"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><p></p><pre></pre></body></html>",
-        "noQuirksBodyHtml": "<p></p><pre></pre>"
-      }
-    },
-    {
-      "data": "<!doctype html><body><p><listing>",
-      "errors": [
-        "(1,33): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "p": true,
-            "listing": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "p"
-                  },
-                  {
-                    "tag": "listing"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><p></p><listing></listing></body></html>",
-        "noQuirksBodyHtml": "<p></p><listing></listing>"
-      }
-    },
-    {
-      "data": "<!doctype html><p><plaintext>",
-      "errors": [
-        "(1,29): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "p": true,
-            "plaintext": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "p"
-                  },
-                  {
-                    "tag": "plaintext"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><p></p><plaintext></plaintext></body></html>",
-        "noQuirksBodyHtml": "<p></p><plaintext></plaintext>"
-      }
-    },
-    {
-      "data": "<!doctype html><p><h1>",
-      "errors": [
-        "(1,22): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "p": true,
-            "h1": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "p"
-                  },
-                  {
-                    "tag": "h1"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><p></p><h1></h1></body></html>",
-        "noQuirksBodyHtml": "<p></p><h1></h1>"
-      }
-    },
-    {
-      "data": "<!doctype html><isindex type=\"hidden\">",
-      "errors": [
-        "(1,38): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "isindex": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "isindex",
-                    "attrs": [
-                      {
-                        "name": "type",
-                        "value": "hidden"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><isindex type=\"hidden\"></isindex></body></html>",
-        "noQuirksBodyHtml": "<isindex type=\"hidden\"></isindex>"
-      }
-    },
-    {
-      "data": "<!doctype html><ruby><p><rp>",
-      "errors": [
-        "(1,28): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "ruby": true,
-            "p": true,
-            "rp": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "ruby",
-                    "children": [
-                      {
-                        "tag": "p"
-                      },
-                      {
-                        "tag": "rp"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><ruby><p></p><rp></rp></ruby></body></html>",
-        "noQuirksBodyHtml": "<ruby><p></p><rp></rp></ruby>"
-      }
-    },
-    {
-      "data": "<!doctype html><ruby><div><span><rp>",
-      "errors": [
-        "(1,36): XXX-undefined-error",
-        "(1,36): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "ruby": true,
-            "div": true,
-            "span": true,
-            "rp": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "ruby",
-                    "children": [
-                      {
-                        "tag": "div",
-                        "children": [
-                          {
-                            "tag": "span",
-                            "children": [
-                              {
-                                "tag": "rp"
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><ruby><div><span><rp></rp></span></div></ruby></body></html>",
-        "noQuirksBodyHtml": "<ruby><div><span><rp></rp></span></div></ruby>"
-      }
-    },
-    {
-      "data": "<!doctype html><ruby><div><p><rp>",
-      "errors": [
-        "(1,33): XXX-undefined-error",
-        "(1,33): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "ruby": true,
-            "div": true,
-            "p": true,
-            "rp": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "ruby",
-                    "children": [
-                      {
-                        "tag": "div",
-                        "children": [
-                          {
-                            "tag": "p"
-                          },
-                          {
-                            "tag": "rp"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><ruby><div><p></p><rp></rp></div></ruby></body></html>",
-        "noQuirksBodyHtml": "<ruby><div><p></p><rp></rp></div></ruby>"
-      }
-    },
-    {
-      "data": "<!doctype html><ruby><p><rt>",
-      "errors": [
-        "(1,28): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "ruby": true,
-            "p": true,
-            "rt": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "ruby",
-                    "children": [
-                      {
-                        "tag": "p"
-                      },
-                      {
-                        "tag": "rt"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><ruby><p></p><rt></rt></ruby></body></html>",
-        "noQuirksBodyHtml": "<ruby><p></p><rt></rt></ruby>"
-      }
-    },
-    {
-      "data": "<!doctype html><ruby><div><span><rt>",
-      "errors": [
-        "(1,36): XXX-undefined-error",
-        "(1,36): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "ruby": true,
-            "div": true,
-            "span": true,
-            "rt": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "ruby",
-                    "children": [
-                      {
-                        "tag": "div",
-                        "children": [
-                          {
-                            "tag": "span",
-                            "children": [
-                              {
-                                "tag": "rt"
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><ruby><div><span><rt></rt></span></div></ruby></body></html>",
-        "noQuirksBodyHtml": "<ruby><div><span><rt></rt></span></div></ruby>"
-      }
-    },
-    {
-      "data": "<!doctype html><ruby><div><p><rt>",
-      "errors": [
-        "(1,33): XXX-undefined-error",
-        "(1,33): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "ruby": true,
-            "div": true,
-            "p": true,
-            "rt": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "ruby",
-                    "children": [
-                      {
-                        "tag": "div",
-                        "children": [
-                          {
-                            "tag": "p"
-                          },
-                          {
-                            "tag": "rt"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><ruby><div><p></p><rt></rt></div></ruby></body></html>",
-        "noQuirksBodyHtml": "<ruby><div><p></p><rt></rt></div></ruby>"
-      }
-    },
-    {
-      "data": "<html><ruby>a<rb>b<rt></ruby></html>",
-      "errors": [
-        "(1,6): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "ruby": true,
-            "rb": true,
-            "rt": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "ruby",
-                    "children": [
-                      {
-                        "text": "a"
-                      },
-                      {
-                        "tag": "rb",
-                        "children": [
-                          {
-                            "text": "b"
-                          }
-                        ]
-                      },
-                      {
-                        "tag": "rt"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><ruby>a<rb>b</rb><rt></rt></ruby></body></html>",
-        "noQuirksBodyHtml": "<ruby>a<rb>b</rb><rt></rt></ruby>"
-      }
-    },
-    {
-      "data": "<html><ruby>a<rp>b<rt></ruby></html>",
-      "errors": [
-        "(1,6): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "ruby": true,
-            "rp": true,
-            "rt": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "ruby",
-                    "children": [
-                      {
-                        "text": "a"
-                      },
-                      {
-                        "tag": "rp",
-                        "children": [
-                          {
-                            "text": "b"
-                          }
-                        ]
-                      },
-                      {
-                        "tag": "rt"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><ruby>a<rp>b</rp><rt></rt></ruby></body></html>",
-        "noQuirksBodyHtml": "<ruby>a<rp>b</rp><rt></rt></ruby>"
-      }
-    },
-    {
-      "data": "<html><ruby>a<rt>b<rt></ruby></html>",
-      "errors": [
-        "(1,6): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "ruby": true,
-            "rt": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "ruby",
-                    "children": [
-                      {
-                        "text": "a"
-                      },
-                      {
-                        "tag": "rt",
-                        "children": [
-                          {
-                            "text": "b"
-                          }
-                        ]
-                      },
-                      {
-                        "tag": "rt"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><ruby>a<rt>b</rt><rt></rt></ruby></body></html>",
-        "noQuirksBodyHtml": "<ruby>a<rt>b</rt><rt></rt></ruby>"
-      }
-    },
-    {
-      "data": "<html><ruby>a<rtc>b<rt>c<rb>d</ruby></html>",
-      "errors": [
-        "(1,6): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "ruby": true,
-            "rtc": true,
-            "rt": true,
-            "rb": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "ruby",
-                    "children": [
-                      {
-                        "text": "a"
-                      },
-                      {
-                        "tag": "rtc",
-                        "children": [
-                          {
-                            "text": "b"
-                          },
-                          {
-                            "tag": "rt",
-                            "children": [
-                              {
-                                "text": "c"
-                              }
-                            ]
-                          }
-                        ]
-                      },
-                      {
-                        "tag": "rb",
-                        "children": [
-                          {
-                            "text": "d"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><ruby>a<rtc>b<rt>c</rt></rtc><rb>d</rb></ruby></body></html>",
-        "noQuirksBodyHtml": "<ruby>a<rtc>b<rt>c</rt></rtc><rb>d</rb></ruby>"
-      }
-    },
-    {
-      "data": "<!doctype html><math/><foo>",
-      "errors": [
-        "(1,27): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "math math": true,
-            "foo": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "math",
-                    "ns": "http://www.w3.org/1998/Math/MathML"
-                  },
-                  {
-                    "tag": "foo"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><math></math><foo></foo></body></html>",
-        "noQuirksBodyHtml": "<math></math><foo></foo>"
-      }
-    },
-    {
-      "data": "<!doctype html><svg/><foo>",
-      "errors": [
-        "(1,26): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "svg svg": true,
-            "foo": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "svg",
-                    "ns": "http://www.w3.org/2000/svg"
-                  },
-                  {
-                    "tag": "foo"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><svg></svg><foo></foo></body></html>",
-        "noQuirksBodyHtml": "<svg></svg><foo></foo>"
-      }
-    },
-    {
-      "data": "<!doctype html><div></body><!--foo-->",
-      "errors": [
-        "(1,27): expected-one-end-tag-but-got-another"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "div": true
-          },
-          "doctype": true,
-          "comment": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "div"
-                  }
-                ]
-              },
-              {
-                "comment": "foo"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><div></div></body><!--foo--></html>",
-        "noQuirksBodyHtml": "<div><!--foo--></div>"
-      }
-    },
-    {
-      "data": "<!doctype html><h1><div><h3><span></h1>foo",
-      "errors": [
-        "(1,39): end-tag-too-early",
-        "(1,42): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "h1": true,
-            "div": true,
-            "h3": true,
-            "span": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "h1",
-                    "children": [
-                      {
-                        "tag": "div",
-                        "children": [
-                          {
-                            "tag": "h3",
-                            "children": [
-                              {
-                                "tag": "span"
-                              }
-                            ]
-                          },
-                          {
-                            "text": "foo"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><h1><div><h3><span></span></h3>foo</div></h1></body></html>",
-        "noQuirksBodyHtml": "<h1><div><h3><span></span></h3>foo</div></h1>"
-      }
-    },
-    {
-      "data": "<!doctype html><p></h3>foo",
-      "errors": [
-        "(1,23): end-tag-too-early"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "p": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "p",
-                    "children": [
-                      {
-                        "text": "foo"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><p>foo</p></body></html>",
-        "noQuirksBodyHtml": "<p>foo</p>"
-      }
-    },
-    {
-      "data": "<!doctype html><h3><li>abc</h2>foo",
-      "errors": [
-        "(1,31): end-tag-too-early"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "h3": true,
-            "li": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "h3",
-                    "children": [
-                      {
-                        "tag": "li",
-                        "children": [
-                          {
-                            "text": "abc"
-                          }
-                        ]
-                      }
-                    ]
-                  },
-                  {
-                    "text": "foo"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><h3><li>abc</li></h3>foo</body></html>",
-        "noQuirksBodyHtml": "<h3><li>abc</li></h3>foo"
-      }
-    },
-    {
-      "data": "<!doctype html><table>abc<!--foo-->",
-      "errors": [
-        "(1,23): foster-parenting-character",
-        "(1,24): foster-parenting-character",
-        "(1,25): foster-parenting-character",
-        "(1,35): eof-in-table"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true
-          },
-          "doctype": true,
-          "comment": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "abc"
-                  },
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "comment": "foo"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body>abc<table><!--foo--></table></body></html>",
-        "noQuirksBodyHtml": "abc<table><!--foo--></table>"
-      }
-    },
-    {
-      "data": "<!doctype html><table>  <!--foo-->",
-      "errors": [
-        "(1,34): eof-in-table"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true
-          },
-          "doctype": true,
-          "comment": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "text": "  "
-                      },
-                      {
-                        "comment": "foo"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><table>  <!--foo--></table></body></html>",
-        "noQuirksBodyHtml": "<table>  <!--foo--></table>"
-      }
-    },
-    {
-      "data": "<!doctype html><table> b <!--foo-->",
-      "errors": [
-        "(1,23): foster-parenting-character",
-        "(1,24): foster-parenting-character",
-        "(1,25): foster-parenting-character",
-        "(1,35): eof-in-table"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true
-          },
-          "doctype": true,
-          "comment": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": " b "
-                  },
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "comment": "foo"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body> b <table><!--foo--></table></body></html>",
-        "noQuirksBodyHtml": " b <table><!--foo--></table>"
-      }
-    },
-    {
-      "data": "<!doctype html><select><option><option>",
-      "errors": [
-        "(1,39): eof-in-select"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "select": true,
-            "option": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "select",
-                    "children": [
-                      {
-                        "tag": "option"
-                      },
-                      {
-                        "tag": "option"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><select><option></option><option></option></select></body></html>",
-        "noQuirksBodyHtml": "<select><option></option><option></option></select>"
-      }
-    },
-    {
-      "data": "<!doctype html><select><option></optgroup>",
-      "errors": [
-        "(1,42): unexpected-end-tag-in-select",
-        "(1,42): eof-in-select"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "select": true,
-            "option": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "select",
-                    "children": [
-                      {
-                        "tag": "option"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><select><option></option></select></body></html>",
-        "noQuirksBodyHtml": "<select><option></option></select>"
-      }
-    },
-    {
-      "data": "<!doctype html><select><option></optgroup>",
-      "errors": [
-        "(1,42): unexpected-end-tag-in-select",
-        "(1,42): eof-in-select"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "select": true,
-            "option": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "select",
-                    "children": [
-                      {
-                        "tag": "option"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><select><option></option></select></body></html>",
-        "noQuirksBodyHtml": "<select><option></option></select>"
-      }
-    },
-    {
-      "data": "<!doctype html><dd><optgroup><dd>",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "dd": true,
-            "optgroup": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "dd",
-                    "children": [
-                      {
-                        "tag": "optgroup"
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "dd"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><dd><optgroup></optgroup></dd><dd></dd></body></html>",
-        "noQuirksBodyHtml": "<dd><optgroup></optgroup></dd><dd></dd>"
-      }
-    },
-    {
-      "data": "<!doctype html><p><math><mi><p><h1>",
-      "errors": [
-        "(1,35): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "p": true,
-            "math math": true,
-            "math mi": true,
-            "h1": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "p",
-                    "children": [
-                      {
-                        "tag": "math",
-                        "ns": "http://www.w3.org/1998/Math/MathML",
-                        "children": [
-                          {
-                            "tag": "mi",
-                            "ns": "http://www.w3.org/1998/Math/MathML",
-                            "children": [
-                              {
-                                "tag": "p"
-                              },
-                              {
-                                "tag": "h1"
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><p><math><mi><p></p><h1></h1></mi></math></p></body></html>",
-        "noQuirksBodyHtml": "<p><math><mi><p></p><h1></h1></mi></math></p>"
-      }
-    },
-    {
-      "data": "<!doctype html><p><math><mo><p><h1>",
-      "errors": [
-        "(1,35): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "p": true,
-            "math math": true,
-            "math mo": true,
-            "h1": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "p",
-                    "children": [
-                      {
-                        "tag": "math",
-                        "ns": "http://www.w3.org/1998/Math/MathML",
-                        "children": [
-                          {
-                            "tag": "mo",
-                            "ns": "http://www.w3.org/1998/Math/MathML",
-                            "children": [
-                              {
-                                "tag": "p"
-                              },
-                              {
-                                "tag": "h1"
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><p><math><mo><p></p><h1></h1></mo></math></p></body></html>",
-        "noQuirksBodyHtml": "<p><math><mo><p></p><h1></h1></mo></math></p>"
-      }
-    },
-    {
-      "data": "<!doctype html><p><math><mn><p><h1>",
-      "errors": [
-        "(1,35): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "p": true,
-            "math math": true,
-            "math mn": true,
-            "h1": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "p",
-                    "children": [
-                      {
-                        "tag": "math",
-                        "ns": "http://www.w3.org/1998/Math/MathML",
-                        "children": [
-                          {
-                            "tag": "mn",
-                            "ns": "http://www.w3.org/1998/Math/MathML",
-                            "children": [
-                              {
-                                "tag": "p"
-                              },
-                              {
-                                "tag": "h1"
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><p><math><mn><p></p><h1></h1></mn></math></p></body></html>",
-        "noQuirksBodyHtml": "<p><math><mn><p></p><h1></h1></mn></math></p>"
-      }
-    },
-    {
-      "data": "<!doctype html><p><math><ms><p><h1>",
-      "errors": [
-        "(1,35): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "p": true,
-            "math math": true,
-            "math ms": true,
-            "h1": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "p",
-                    "children": [
-                      {
-                        "tag": "math",
-                        "ns": "http://www.w3.org/1998/Math/MathML",
-                        "children": [
-                          {
-                            "tag": "ms",
-                            "ns": "http://www.w3.org/1998/Math/MathML",
-                            "children": [
-                              {
-                                "tag": "p"
-                              },
-                              {
-                                "tag": "h1"
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><p><math><ms><p></p><h1></h1></ms></math></p></body></html>",
-        "noQuirksBodyHtml": "<p><math><ms><p></p><h1></h1></ms></math></p>"
-      }
-    },
-    {
-      "data": "<!doctype html><p><math><mtext><p><h1>",
-      "errors": [
-        "(1,38): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "p": true,
-            "math math": true,
-            "math mtext": true,
-            "h1": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "p",
-                    "children": [
-                      {
-                        "tag": "math",
-                        "ns": "http://www.w3.org/1998/Math/MathML",
-                        "children": [
-                          {
-                            "tag": "mtext",
-                            "ns": "http://www.w3.org/1998/Math/MathML",
-                            "children": [
-                              {
-                                "tag": "p"
-                              },
-                              {
-                                "tag": "h1"
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><p><math><mtext><p></p><h1></h1></mtext></math></p></body></html>",
-        "noQuirksBodyHtml": "<p><math><mtext><p></p><h1></h1></mtext></math></p>"
-      }
-    },
-    {
-      "data": "<!doctype html><frameset></noframes>",
-      "errors": [
-        "(1,36): unexpected-end-tag-in-frameset",
-        "(1,36): eof-in-frameset"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "frameset": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "frameset"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><frameset></frameset></html>",
-        "noQuirksBodyHtml": ""
-      }
-    },
-    {
-      "data": "<!doctype html><html c=d><body></html><html a=b>",
-      "errors": [
-        "(1,48): non-html-root"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "attrs": [
-              {
-                "name": "a",
-                "value": "b"
-              },
-              {
-                "name": "c",
-                "value": "d"
-              }
-            ],
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html c=\"d\" a=\"b\"><head></head><body></body></html>",
-        "noQuirksBodyHtml": ""
-      }
-    },
-    {
-      "data": "<!doctype html><html c=d><frameset></frameset></html><html a=b>",
-      "errors": [
-        "(1,63): non-html-root"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "frameset": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "attrs": [
-              {
-                "name": "a",
-                "value": "b"
-              },
-              {
-                "name": "c",
-                "value": "d"
-              }
-            ],
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "frameset"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html c=\"d\" a=\"b\"><head></head><frameset></frameset></html>",
-        "noQuirksBodyHtml": ""
-      }
-    },
-    {
-      "data": "<!doctype html><html><frameset></frameset></html><!--foo-->",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "frameset": true
-          },
-          "doctype": true,
-          "comment": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "frameset"
-              }
-            ]
-          },
-          {
-            "comment": "foo"
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><frameset></frameset></html><!--foo-->",
-        "noQuirksBodyHtml": "<!--foo-->"
-      }
-    },
-    {
-      "data": "<!doctype html><html><frameset></frameset></html>  ",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "frameset": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "frameset"
-              },
-              {
-                "text": "  "
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><frameset></frameset>  </html>",
-        "noQuirksBodyHtml": "  "
-      }
-    },
-    {
-      "data": "<!doctype html><html><frameset></frameset></html>abc",
-      "errors": [
-        "(1,50): expected-eof-but-got-char",
-        "(1,51): expected-eof-but-got-char",
-        "(1,52): expected-eof-but-got-char"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "frameset": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "frameset"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><frameset></frameset></html>",
-        "noQuirksBodyHtml": "abc"
-      }
-    },
-    {
-      "data": "<!doctype html><html><frameset></frameset></html><p>",
-      "errors": [
-        "(1,52): expected-eof-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "frameset": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "frameset"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><frameset></frameset></html>",
-        "noQuirksBodyHtml": "<p></p>"
-      }
-    },
-    {
-      "data": "<!doctype html><html><frameset></frameset></html></p>",
-      "errors": [
-        "(1,53): expected-eof-but-got-end-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "frameset": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "frameset"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><frameset></frameset></html>",
-        "noQuirksBodyHtml": "<p></p>"
-      }
-    },
-    {
-      "data": "<html><frameset></frameset></html><!doctype html>",
-      "errors": [
-        "(1,6): expected-doctype-but-got-start-tag",
-        "(1,49): unexpected-doctype"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "frameset": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "frameset"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><frameset></frameset></html>",
-        "noQuirksBodyHtml": ""
-      }
-    },
-    {
-      "data": "<!doctype html><body><frameset>",
-      "errors": [
-        "(1,31): unexpected-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body></body></html>",
-        "noQuirksBodyHtml": ""
-      }
-    },
-    {
-      "data": "<!doctype html><p><frameset><frame>",
-      "errors": [
-        "(1,28): unexpected-start-tag",
-        "(1,35): eof-in-frameset"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "frameset": true,
-            "frame": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "frameset",
-                "children": [
-                  {
-                    "tag": "frame"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><frameset><frame></frameset></html>",
-        "noQuirksBodyHtml": "<p></p>"
-      }
-    },
-    {
-      "data": "<!doctype html><p>a<frameset>",
-      "errors": [
-        "(1,29): unexpected-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "p": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "p",
-                    "children": [
-                      {
-                        "text": "a"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><p>a</p></body></html>",
-        "noQuirksBodyHtml": "<p>a</p>"
-      }
-    },
-    {
-      "data": "<!doctype html><p> <frameset><frame>",
-      "errors": [
-        "(1,29): unexpected-start-tag",
-        "(1,36): eof-in-frameset"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "frameset": true,
-            "frame": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "frameset",
-                "children": [
-                  {
-                    "tag": "frame"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><frameset><frame></frameset></html>",
-        "noQuirksBodyHtml": "<p> </p>"
-      }
-    },
-    {
-      "data": "<!doctype html><pre><frameset>",
-      "errors": [
-        "(1,30): unexpected-start-tag",
-        "(1,30): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "pre": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "pre"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><pre></pre></body></html>",
-        "noQuirksBodyHtml": "<pre></pre>"
-      }
-    },
-    {
-      "data": "<!doctype html><listing><frameset>",
-      "errors": [
-        "(1,34): unexpected-start-tag",
-        "(1,34): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "listing": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "listing"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><listing></listing></body></html>",
-        "noQuirksBodyHtml": "<listing></listing>"
-      }
-    },
-    {
-      "data": "<!doctype html><li><frameset>",
-      "errors": [
-        "(1,29): unexpected-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "li": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "li"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><li></li></body></html>",
-        "noQuirksBodyHtml": "<li></li>"
-      }
-    },
-    {
-      "data": "<!doctype html><dd><frameset>",
-      "errors": [
-        "(1,29): unexpected-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "dd": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "dd"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><dd></dd></body></html>",
-        "noQuirksBodyHtml": "<dd></dd>"
-      }
-    },
-    {
-      "data": "<!doctype html><dt><frameset>",
-      "errors": [
-        "(1,29): unexpected-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "dt": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "dt"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><dt></dt></body></html>",
-        "noQuirksBodyHtml": "<dt></dt>"
-      }
-    },
-    {
-      "data": "<!doctype html><button><frameset>",
-      "errors": [
-        "(1,33): unexpected-start-tag",
-        "(1,33): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "button": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "button"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><button></button></body></html>",
-        "noQuirksBodyHtml": "<button></button>"
-      }
-    },
-    {
-      "data": "<!doctype html><applet><frameset>",
-      "errors": [
-        "(1,33): unexpected-start-tag",
-        "(1,33): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "applet": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "applet"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><applet></applet></body></html>",
-        "noQuirksBodyHtml": "<applet></applet>"
-      }
-    },
-    {
-      "data": "<!doctype html><marquee><frameset>",
-      "errors": [
-        "(1,34): unexpected-start-tag",
-        "(1,34): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "marquee": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "marquee"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><marquee></marquee></body></html>",
-        "noQuirksBodyHtml": "<marquee></marquee>"
-      }
-    },
-    {
-      "data": "<!doctype html><object><frameset>",
-      "errors": [
-        "(1,33): unexpected-start-tag",
-        "(1,33): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "object": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "object"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><object></object></body></html>",
-        "noQuirksBodyHtml": "<object></object>"
-      }
-    },
-    {
-      "data": "<!doctype html><table><frameset>",
-      "errors": [
-        "(1,32): unexpected-start-tag-implies-table-voodoo",
-        "(1,32): unexpected-start-tag",
-        "(1,32): eof-in-table"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "table"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><table></table></body></html>",
-        "noQuirksBodyHtml": "<table></table>"
-      }
-    },
-    {
-      "data": "<!doctype html><area><frameset>",
-      "errors": [
-        "(1,31): unexpected-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "area": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "area"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><area></body></html>",
-        "noQuirksBodyHtml": "<area>"
-      }
-    },
-    {
-      "data": "<!doctype html><basefont><frameset>",
-      "errors": [
-        "(1,35): eof-in-frameset"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "basefont": true,
-            "frameset": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "basefont"
-                  }
-                ]
-              },
-              {
-                "tag": "frameset"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head><basefont></head><frameset></frameset></html>",
-        "noQuirksBodyHtml": "<basefont>"
-      }
-    },
-    {
-      "data": "<!doctype html><bgsound><frameset>",
-      "errors": [
-        "(1,34): eof-in-frameset"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "bgsound": true,
-            "frameset": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "bgsound"
-                  }
-                ]
-              },
-              {
-                "tag": "frameset"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head><bgsound></head><frameset></frameset></html>",
-        "noQuirksBodyHtml": "<bgsound>"
-      }
-    },
-    {
-      "data": "<!doctype html><br><frameset>",
-      "errors": [
-        "(1,29): unexpected-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "br": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "br"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><br></body></html>",
-        "noQuirksBodyHtml": "<br>"
-      }
-    },
-    {
-      "data": "<!doctype html><embed><frameset>",
-      "errors": [
-        "(1,32): unexpected-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "embed": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "embed"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><embed></body></html>",
-        "noQuirksBodyHtml": "<embed>"
-      }
-    },
-    {
-      "data": "<!doctype html><img><frameset>",
-      "errors": [
-        "(1,30): unexpected-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "img": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "img"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><img></body></html>",
-        "noQuirksBodyHtml": "<img>"
-      }
-    },
-    {
-      "data": "<!doctype html><input><frameset>",
-      "errors": [
-        "(1,32): unexpected-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "input": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "input"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><input></body></html>",
-        "noQuirksBodyHtml": "<input>"
-      }
-    },
-    {
-      "data": "<!doctype html><keygen><frameset>",
-      "errors": [
-        "(1,33): unexpected-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "keygen": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "keygen"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><keygen></body></html>",
-        "noQuirksBodyHtml": "<keygen>"
-      }
-    },
-    {
-      "data": "<!doctype html><wbr><frameset>",
-      "errors": [
-        "(1,30): unexpected-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "wbr": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "wbr"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><wbr></body></html>",
-        "noQuirksBodyHtml": "<wbr>"
-      }
-    },
-    {
-      "data": "<!doctype html><hr><frameset>",
-      "errors": [
-        "(1,29): unexpected-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "hr": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "hr"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><hr></body></html>",
-        "noQuirksBodyHtml": "<hr>"
-      }
-    },
-    {
-      "data": "<!doctype html><textarea></textarea><frameset>",
-      "errors": [
-        "(1,46): unexpected-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "textarea": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "textarea"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><textarea></textarea></body></html>",
-        "noQuirksBodyHtml": "<textarea></textarea>"
-      }
-    },
-    {
-      "data": "<!doctype html><xmp></xmp><frameset>",
-      "errors": [
-        "(1,36): unexpected-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "xmp": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "xmp"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><xmp></xmp></body></html>",
-        "noQuirksBodyHtml": "<xmp></xmp>"
-      }
-    },
-    {
-      "data": "<!doctype html><iframe></iframe><frameset>",
-      "errors": [
-        "(1,42): unexpected-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "iframe": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "iframe"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><iframe></iframe></body></html>",
-        "noQuirksBodyHtml": "<iframe></iframe>"
-      }
-    },
-    {
-      "data": "<!doctype html><select></select><frameset>",
-      "errors": [
-        "(1,42): unexpected-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "select": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "select"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><select></select></body></html>",
-        "noQuirksBodyHtml": "<select></select>"
-      }
-    },
-    {
-      "data": "<!doctype html><svg></svg><frameset><frame>",
-      "errors": [
-        "(1,36): unexpected-start-tag",
-        "(1,43): eof-in-frameset"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "frameset": true,
-            "frame": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "frameset",
-                "children": [
-                  {
-                    "tag": "frame"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><frameset><frame></frameset></html>",
-        "noQuirksBodyHtml": "<svg></svg>"
-      }
-    },
-    {
-      "data": "<!doctype html><math></math><frameset><frame>",
-      "errors": [
-        "(1,38): unexpected-start-tag",
-        "(1,45): eof-in-frameset"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "frameset": true,
-            "frame": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "frameset",
-                "children": [
-                  {
-                    "tag": "frame"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><frameset><frame></frameset></html>",
-        "noQuirksBodyHtml": "<math></math>"
-      }
-    },
-    {
-      "data": "<!doctype html><svg><foreignObject><div> <frameset><frame>",
-      "errors": [
-        "(1,51): unexpected-start-tag",
-        "(1,58): eof-in-frameset"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "frameset": true,
-            "frame": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "frameset",
-                "children": [
-                  {
-                    "tag": "frame"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><frameset><frame></frameset></html>",
-        "noQuirksBodyHtml": "<svg><foreignObject><div> </div></foreignObject></svg>"
-      }
-    },
-    {
-      "data": "<!doctype html><svg>a</svg><frameset><frame>",
-      "errors": [
-        "(1,37): unexpected-start-tag",
-        "(1,44): unexpected-start-tag-ignored"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "svg svg": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "svg",
-                    "ns": "http://www.w3.org/2000/svg",
-                    "children": [
-                      {
-                        "text": "a"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><svg>a</svg></body></html>",
-        "noQuirksBodyHtml": "<svg>a</svg>"
-      }
-    },
-    {
-      "data": "<!doctype html><svg> </svg><frameset><frame>",
-      "errors": [
-        "(1,37): unexpected-start-tag",
-        "(1,44): eof-in-frameset"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "frameset": true,
-            "frame": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "frameset",
-                "children": [
-                  {
-                    "tag": "frame"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><frameset><frame></frameset></html>",
-        "noQuirksBodyHtml": "<svg> </svg>"
-      }
-    },
-    {
-      "data": "<html>aaa<frameset></frameset>",
-      "errors": [
-        "(1,6): expected-doctype-but-got-start-tag",
-        "(1,19): unexpected-start-tag",
-        "(1,30): unexpected-end-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "aaa"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>aaa</body></html>",
-        "noQuirksBodyHtml": "aaa"
-      }
-    },
-    {
-      "data": "<html> a <frameset></frameset>",
-      "errors": [
-        "(1,6): expected-doctype-but-got-start-tag",
-        "(1,19): unexpected-start-tag",
-        "(1,30): unexpected-end-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "a "
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>a </body></html>",
-        "noQuirksBodyHtml": " a "
-      }
-    },
-    {
-      "data": "<!doctype html><div><frameset>",
-      "errors": [
-        "(1,30): unexpected-start-tag",
-        "(1,30): eof-in-frameset"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "frameset": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "frameset"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><frameset></frameset></html>",
-        "noQuirksBodyHtml": "<div></div>"
-      }
-    },
-    {
-      "data": "<!doctype html><div><body><frameset>",
-      "errors": [
-        "(1,26): unexpected-start-tag",
-        "(1,36): unexpected-start-tag",
-        "(1,36): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "div": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "div"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><div></div></body></html>",
-        "noQuirksBodyHtml": "<div></div>"
-      }
-    },
-    {
-      "data": "<!doctype html><p><math></p>a",
-      "errors": [
-        "(1,28): unexpected-end-tag",
-        "(1,28): unexpected-end-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "p": true,
-            "math math": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "p",
-                    "children": [
-                      {
-                        "tag": "math",
-                        "ns": "http://www.w3.org/1998/Math/MathML"
-                      }
-                    ]
-                  },
-                  {
-                    "text": "a"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><p><math></math></p>a</body></html>",
-        "noQuirksBodyHtml": "<p><math></math></p>a"
-      }
-    },
-    {
-      "data": "<!doctype html><p><math><mn><span></p>a",
-      "errors": [
-        "(1,38): unexpected-end-tag",
-        "(1,39): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "p": true,
-            "math math": true,
-            "math mn": true,
-            "span": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "p",
-                    "children": [
-                      {
-                        "tag": "math",
-                        "ns": "http://www.w3.org/1998/Math/MathML",
-                        "children": [
-                          {
-                            "tag": "mn",
-                            "ns": "http://www.w3.org/1998/Math/MathML",
-                            "children": [
-                              {
-                                "tag": "span",
-                                "children": [
-                                  {
-                                    "tag": "p"
-                                  },
-                                  {
-                                    "text": "a"
-                                  }
-                                ]
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><p><math><mn><span><p></p>a</span></mn></math></p></body></html>",
-        "noQuirksBodyHtml": "<p><math><mn><span><p></p>a</span></mn></math></p>"
-      }
-    },
-    {
-      "data": "<!doctype html><math></html>",
-      "errors": [
-        "(1,28): unexpected-end-tag",
-        "(1,28): expected-one-end-tag-but-got-another",
-        "(1,28): unexpected-end-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "math math": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "math",
-                    "ns": "http://www.w3.org/1998/Math/MathML"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><math></math></body></html>",
-        "noQuirksBodyHtml": "<math></math>"
-      }
-    },
-    {
-      "data": "<!doctype html><meta charset=\"ascii\">",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "meta": true,
-            "body": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "meta",
-                    "attrs": [
-                      {
-                        "name": "charset",
-                        "value": "ascii"
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head><meta charset=\"ascii\"></head><body></body></html>",
-        "noQuirksBodyHtml": "<meta charset=\"ascii\">"
-      }
-    },
-    {
-      "data": "<!doctype html><meta http-equiv=\"content-type\" content=\"text/html;charset=ascii\">",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "meta": true,
-            "body": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "meta",
-                    "attrs": [
-                      {
-                        "name": "content",
-                        "value": "text/html;charset=ascii"
-                      },
-                      {
-                        "name": "http-equiv",
-                        "value": "content-type"
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head><meta http-equiv=\"content-type\" content=\"text/html;charset=ascii\"></head><body></body></html>",
-        "noQuirksBodyHtml": "<meta http-equiv=\"content-type\" content=\"text/html;charset=ascii\">"
-      }
-    },
-    {
-      "data": "<!doctype html><head><!--aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa--><meta charset=\"utf8\">",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "meta": true,
-            "body": true
-          },
-          "doctype": true,
-          "comment": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "comment": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
-                  },
-                  {
-                    "tag": "meta",
-                    "attrs": [
-                      {
-                        "name": "charset",
-                        "value": "utf8"
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head><!--aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa--><meta charset=\"utf8\"></head><body></body></html>",
-        "noQuirksBodyHtml": "<!--aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa--><meta charset=\"utf8\">"
-      }
-    },
-    {
-      "data": "<!doctype html><html a=b><head></head><html c=d>",
-      "errors": [
-        "(1,48): non-html-root"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "attrs": [
-              {
-                "name": "a",
-                "value": "b"
-              },
-              {
-                "name": "c",
-                "value": "d"
-              }
-            ],
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html a=\"b\" c=\"d\"><head></head><body></body></html>",
-        "noQuirksBodyHtml": ""
-      }
-    },
-    {
-      "data": "<!doctype html><image/>",
-      "errors": [
-        "(1,23): image-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "img": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "img"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><img></body></html>",
-        "noQuirksBodyHtml": "<img>"
-      }
-    },
-    {
-      "data": "<!doctype html>a<i>b<table>c<b>d</i>e</b>f",
-      "errors": [
-        "(1,28): foster-parenting-character",
-        "(1,31): foster-parenting-start-tag",
-        "(1,32): foster-parenting-character",
-        "(1,36): foster-parenting-end-tag",
-        "(1,36): adoption-agency-1.3",
-        "(1,37): foster-parenting-character",
-        "(1,41): foster-parenting-end-tag",
-        "(1,42): foster-parenting-character",
-        "(1,42): eof-in-table"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "i": true,
-            "b": true,
-            "table": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "a"
-                  },
-                  {
-                    "tag": "i",
-                    "children": [
-                      {
-                        "text": "bc"
-                      },
-                      {
-                        "tag": "b",
-                        "children": [
-                          {
-                            "text": "de"
-                          }
-                        ]
-                      },
-                      {
-                        "text": "f"
-                      },
-                      {
-                        "tag": "table"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body>a<i>bc<b>de</b>f<table></table></i></body></html>",
-        "noQuirksBodyHtml": "a<i>bc<b>de</b>f<table></table></i>"
-      }
-    },
-    {
-      "data": "<!doctype html><table><i>a<b>b<div>c<a>d</i>e</b>f",
-      "errors": [
-        "(1,25): foster-parenting-start-tag",
-        "(1,26): foster-parenting-character",
-        "(1,29): foster-parenting-start-tag",
-        "(1,30): foster-parenting-character",
-        "(1,35): foster-parenting-start-tag",
-        "(1,36): foster-parenting-character",
-        "(1,39): foster-parenting-start-tag",
-        "(1,40): foster-parenting-character",
-        "(1,44): foster-parenting-end-tag",
-        "(1,44): adoption-agency-1.3",
-        "(1,44): adoption-agency-1.3",
-        "(1,45): foster-parenting-character",
-        "(1,49): foster-parenting-end-tag",
-        "(1,49): adoption-agency-1.3",
-        "(1,49): adoption-agency-1.3",
-        "(1,50): foster-parenting-character",
-        "(1,50): eof-in-table"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "i": true,
-            "b": true,
-            "div": true,
-            "a": true,
-            "table": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "i",
-                    "children": [
-                      {
-                        "text": "a"
-                      },
-                      {
-                        "tag": "b",
-                        "children": [
-                          {
-                            "text": "b"
-                          }
-                        ]
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "b"
-                  },
-                  {
-                    "tag": "div",
-                    "children": [
-                      {
-                        "tag": "b",
-                        "children": [
-                          {
-                            "tag": "i",
-                            "children": [
-                              {
-                                "text": "c"
-                              },
-                              {
-                                "tag": "a",
-                                "children": [
-                                  {
-                                    "text": "d"
-                                  }
-                                ]
-                              }
-                            ]
-                          },
-                          {
-                            "tag": "a",
-                            "children": [
-                              {
-                                "text": "e"
-                              }
-                            ]
-                          }
-                        ]
-                      },
-                      {
-                        "tag": "a",
-                        "children": [
-                          {
-                            "text": "f"
-                          }
-                        ]
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "table"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><i>a<b>b</b></i><b></b><div><b><i>c<a>d</a></i><a>e</a></b><a>f</a></div><table></table></body></html>",
-        "noQuirksBodyHtml": "<i>a<b>b</b></i><b></b><div><b><i>c<a>d</a></i><a>e</a></b><a>f</a></div><table></table>"
-      }
-    },
-    {
-      "data": "<!doctype html><i>a<b>b<div>c<a>d</i>e</b>f",
-      "errors": [
-        "(1,37): adoption-agency-1.3",
-        "(1,37): adoption-agency-1.3",
-        "(1,42): adoption-agency-1.3",
-        "(1,42): adoption-agency-1.3",
-        "(1,43): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "i": true,
-            "b": true,
-            "div": true,
-            "a": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "i",
-                    "children": [
-                      {
-                        "text": "a"
-                      },
-                      {
-                        "tag": "b",
-                        "children": [
-                          {
-                            "text": "b"
-                          }
-                        ]
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "b"
-                  },
-                  {
-                    "tag": "div",
-                    "children": [
-                      {
-                        "tag": "b",
-                        "children": [
-                          {
-                            "tag": "i",
-                            "children": [
-                              {
-                                "text": "c"
-                              },
-                              {
-                                "tag": "a",
-                                "children": [
-                                  {
-                                    "text": "d"
-                                  }
-                                ]
-                              }
-                            ]
-                          },
-                          {
-                            "tag": "a",
-                            "children": [
-                              {
-                                "text": "e"
-                              }
-                            ]
-                          }
-                        ]
-                      },
-                      {
-                        "tag": "a",
-                        "children": [
-                          {
-                            "text": "f"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><i>a<b>b</b></i><b></b><div><b><i>c<a>d</a></i><a>e</a></b><a>f</a></div></body></html>",
-        "noQuirksBodyHtml": "<i>a<b>b</b></i><b></b><div><b><i>c<a>d</a></i><a>e</a></b><a>f</a></div>"
-      }
-    },
-    {
-      "data": "<!doctype html><table><i>a<b>b<div>c</i>",
-      "errors": [
-        "(1,25): foster-parenting-start-tag",
-        "(1,26): foster-parenting-character",
-        "(1,29): foster-parenting-start-tag",
-        "(1,30): foster-parenting-character",
-        "(1,35): foster-parenting-start-tag",
-        "(1,36): foster-parenting-character",
-        "(1,40): foster-parenting-end-tag",
-        "(1,40): adoption-agency-1.3",
-        "(1,40): eof-in-table"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "i": true,
-            "b": true,
-            "div": true,
-            "table": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "i",
-                    "children": [
-                      {
-                        "text": "a"
-                      },
-                      {
-                        "tag": "b",
-                        "children": [
-                          {
-                            "text": "b"
-                          }
-                        ]
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "b",
-                    "children": [
-                      {
-                        "tag": "div",
-                        "children": [
-                          {
-                            "tag": "i",
-                            "children": [
-                              {
-                                "text": "c"
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "table"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><i>a<b>b</b></i><b><div><i>c</i></div></b><table></table></body></html>",
-        "noQuirksBodyHtml": "<i>a<b>b</b></i><b><div><i>c</i></div></b><table></table>"
-      }
-    },
-    {
-      "data": "<!doctype html><table><i>a<b>b<div>c<a>d</i>e</b>f",
-      "errors": [
-        "(1,25): foster-parenting-start-tag",
-        "(1,26): foster-parenting-character",
-        "(1,29): foster-parenting-start-tag",
-        "(1,30): foster-parenting-character",
-        "(1,35): foster-parenting-start-tag",
-        "(1,36): foster-parenting-character",
-        "(1,39): foster-parenting-start-tag",
-        "(1,40): foster-parenting-character",
-        "(1,44): foster-parenting-end-tag",
-        "(1,44): adoption-agency-1.3",
-        "(1,44): adoption-agency-1.3",
-        "(1,45): foster-parenting-character",
-        "(1,49): foster-parenting-end-tag",
-        "(1,44): adoption-agency-1.3",
-        "(1,44): adoption-agency-1.3",
-        "(1,50): foster-parenting-character",
-        "(1,50): eof-in-table"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "i": true,
-            "b": true,
-            "div": true,
-            "a": true,
-            "table": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "i",
-                    "children": [
-                      {
-                        "text": "a"
-                      },
-                      {
-                        "tag": "b",
-                        "children": [
-                          {
-                            "text": "b"
-                          }
-                        ]
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "b"
-                  },
-                  {
-                    "tag": "div",
-                    "children": [
-                      {
-                        "tag": "b",
-                        "children": [
-                          {
-                            "tag": "i",
-                            "children": [
-                              {
-                                "text": "c"
-                              },
-                              {
-                                "tag": "a",
-                                "children": [
-                                  {
-                                    "text": "d"
-                                  }
-                                ]
-                              }
-                            ]
-                          },
-                          {
-                            "tag": "a",
-                            "children": [
-                              {
-                                "text": "e"
-                              }
-                            ]
-                          }
-                        ]
-                      },
-                      {
-                        "tag": "a",
-                        "children": [
-                          {
-                            "text": "f"
-                          }
-                        ]
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "table"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><i>a<b>b</b></i><b></b><div><b><i>c<a>d</a></i><a>e</a></b><a>f</a></div><table></table></body></html>",
-        "noQuirksBodyHtml": "<i>a<b>b</b></i><b></b><div><b><i>c<a>d</a></i><a>e</a></b><a>f</a></div><table></table>"
-      }
-    },
-    {
-      "data": "<!doctype html><table><i>a<div>b<tr>c<b>d</i>e",
-      "errors": [
-        "(1,25): foster-parenting-start-tag",
-        "(1,26): foster-parenting-character",
-        "(1,31): foster-parenting-start-tag",
-        "(1,32): foster-parenting-character",
-        "(1,37): foster-parenting-character",
-        "(1,40): foster-parenting-start-tag",
-        "(1,41): foster-parenting-character",
-        "(1,45): foster-parenting-end-tag",
-        "(1,45): adoption-agency-1.3",
-        "(1,46): foster-parenting-character",
-        "(1,46): eof-in-table"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "i": true,
-            "div": true,
-            "b": true,
-            "table": true,
-            "tbody": true,
-            "tr": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "i",
-                    "children": [
-                      {
-                        "text": "a"
-                      },
-                      {
-                        "tag": "div",
-                        "children": [
-                          {
-                            "text": "b"
-                          }
-                        ]
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "i",
-                    "children": [
-                      {
-                        "text": "c"
-                      },
-                      {
-                        "tag": "b",
-                        "children": [
-                          {
-                            "text": "d"
-                          }
-                        ]
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "b",
-                    "children": [
-                      {
-                        "text": "e"
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "tbody",
-                        "children": [
-                          {
-                            "tag": "tr"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><i>a<div>b</div></i><i>c<b>d</b></i><b>e</b><table><tbody><tr></tr></tbody></table></body></html>",
-        "noQuirksBodyHtml": "<i>a<div>b</div></i><i>c<b>d</b></i><b>e</b><table><tbody><tr></tr></tbody></table>"
-      }
-    },
-    {
-      "data": "<!doctype html><table><td><table><i>a<div>b<b>c</i>d",
-      "errors": [
-        "(1,26): unexpected-cell-in-table-body",
-        "(1,36): foster-parenting-start-tag",
-        "(1,37): foster-parenting-character",
-        "(1,42): foster-parenting-start-tag",
-        "(1,43): foster-parenting-character",
-        "(1,46): foster-parenting-start-tag",
-        "(1,47): foster-parenting-character",
-        "(1,51): foster-parenting-end-tag",
-        "(1,51): adoption-agency-1.3",
-        "(1,51): adoption-agency-1.3",
-        "(1,52): foster-parenting-character",
-        "(1,52): eof-in-table"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true,
-            "tbody": true,
-            "tr": true,
-            "td": true,
-            "i": true,
-            "div": true,
-            "b": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "tbody",
-                        "children": [
-                          {
-                            "tag": "tr",
-                            "children": [
-                              {
-                                "tag": "td",
-                                "children": [
-                                  {
-                                    "tag": "i",
-                                    "children": [
-                                      {
-                                        "text": "a"
-                                      }
-                                    ]
-                                  },
-                                  {
-                                    "tag": "div",
-                                    "children": [
-                                      {
-                                        "tag": "i",
-                                        "children": [
-                                          {
-                                            "text": "b"
-                                          },
-                                          {
-                                            "tag": "b",
-                                            "children": [
-                                              {
-                                                "text": "c"
-                                              }
-                                            ]
-                                          }
-                                        ]
-                                      },
-                                      {
-                                        "tag": "b",
-                                        "children": [
-                                          {
-                                            "text": "d"
-                                          }
-                                        ]
-                                      }
-                                    ]
-                                  },
-                                  {
-                                    "tag": "table"
-                                  }
-                                ]
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><table><tbody><tr><td><i>a</i><div><i>b<b>c</b></i><b>d</b></div><table></table></td></tr></tbody></table></body></html>",
-        "noQuirksBodyHtml": "<table><tbody><tr><td><i>a</i><div><i>b<b>c</b></i><b>d</b></div><table></table></td></tr></tbody></table>"
-      }
-    },
-    {
-      "data": "<!doctype html><body><bgsound>",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "bgsound": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "bgsound"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><bgsound></body></html>",
-        "noQuirksBodyHtml": "<bgsound>"
-      }
-    },
-    {
-      "data": "<!doctype html><body><basefont>",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "basefont": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "basefont"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><basefont></body></html>",
-        "noQuirksBodyHtml": "<basefont>"
-      }
-    },
-    {
-      "data": "<!doctype html><a><b></a><basefont>",
-      "errors": [
-        "(1,25): adoption-agency-1.3"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "a": true,
-            "b": true,
-            "basefont": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "a",
-                    "children": [
-                      {
-                        "tag": "b"
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "basefont"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><a><b></b></a><basefont></body></html>",
-        "noQuirksBodyHtml": "<a><b></b></a><basefont>"
-      }
-    },
-    {
-      "data": "<!doctype html><a><b></a><bgsound>",
-      "errors": [
-        "(1,25): adoption-agency-1.3"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "a": true,
-            "b": true,
-            "bgsound": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "a",
-                    "children": [
-                      {
-                        "tag": "b"
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "bgsound"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><a><b></b></a><bgsound></body></html>",
-        "noQuirksBodyHtml": "<a><b></b></a><bgsound>"
-      }
-    },
-    {
-      "data": "<!doctype html><figcaption><article></figcaption>a",
-      "errors": [
-        "(1,49): end-tag-too-early"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "figcaption": true,
-            "article": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "figcaption",
-                    "children": [
-                      {
-                        "tag": "article"
-                      }
-                    ]
-                  },
-                  {
-                    "text": "a"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><figcaption><article></article></figcaption>a</body></html>",
-        "noQuirksBodyHtml": "<figcaption><article></article></figcaption>a"
-      }
-    },
-    {
-      "data": "<!doctype html><summary><article></summary>a",
-      "errors": [
-        "(1,43): end-tag-too-early"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "summary": true,
-            "article": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "summary",
-                    "children": [
-                      {
-                        "tag": "article"
-                      }
-                    ]
-                  },
-                  {
-                    "text": "a"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><summary><article></article></summary>a</body></html>",
-        "noQuirksBodyHtml": "<summary><article></article></summary>a"
-      }
-    },
-    {
-      "data": "<!doctype html><p><a><plaintext>b",
-      "errors": [
-        "(1,32): unexpected-end-tag",
-        "(1,33): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "p": true,
-            "a": true,
-            "plaintext": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "p",
-                    "children": [
-                      {
-                        "tag": "a"
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "plaintext",
-                    "children": [
-                      {
-                        "tag": "a",
-                        "children": [
-                          {
-                            "text": "b"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><p><a></a></p><plaintext><a>b</a></plaintext></body></html>",
-        "noQuirksBodyHtml": "<p><a></a></p><plaintext><a>b</a></plaintext>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><div>a<a></div>b<p>c</p>d",
-      "errors": [
-        "(1,30): end-tag-too-early",
-        "(1,40): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "div": true,
-            "a": true,
-            "p": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "div",
-                    "children": [
-                      {
-                        "text": "a"
-                      },
-                      {
-                        "tag": "a"
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "a",
-                    "children": [
-                      {
-                        "text": "b"
-                      },
-                      {
-                        "tag": "p",
-                        "children": [
-                          {
-                            "text": "c"
-                          }
-                        ]
-                      },
-                      {
-                        "text": "d"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><div>a<a></a></div><a>b<p>c</p>d</a></body></html>",
-        "noQuirksBodyHtml": "<div>a<a></a></div><a>b<p>c</p>d</a>"
-      }
-    }
-  ],
-  "tests2.dat": [
-    {
-      "data": "<!DOCTYPE html>Test",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "Test"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body>Test</body></html>",
-        "noQuirksBodyHtml": "Test"
-      }
-    },
-    {
-      "data": "<textarea>test</div>test",
-      "errors": [
-        "(1,10): expected-doctype-but-got-start-tag",
-        "(1,24): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "textarea": true
-          },
-          "escaped": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "textarea",
-                    "children": [
-                      {
-                        "text": "test</div>test",
-                        "escaped": true
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><textarea>test&lt;/div&gt;test</textarea></body></html>",
-        "noQuirksBodyHtml": "<textarea>test&lt;/div&gt;test</textarea>"
-      }
-    },
-    {
-      "data": "<table><td>",
-      "errors": [
-        "(1,7): expected-doctype-but-got-start-tag",
-        "(1,11): unexpected-cell-in-table-body",
-        "(1,11): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true,
-            "tbody": true,
-            "tr": true,
-            "td": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "tbody",
-                        "children": [
-                          {
-                            "tag": "tr",
-                            "children": [
-                              {
-                                "tag": "td"
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><table><tbody><tr><td></td></tr></tbody></table></body></html>",
-        "noQuirksBodyHtml": "<table><tbody><tr><td></td></tr></tbody></table>"
-      }
-    },
-    {
-      "data": "<table><td>test</tbody></table>",
-      "errors": [
-        "(1,7): expected-doctype-but-got-start-tag",
-        "(1,11): unexpected-cell-in-table-body"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true,
-            "tbody": true,
-            "tr": true,
-            "td": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "tbody",
-                        "children": [
-                          {
-                            "tag": "tr",
-                            "children": [
-                              {
-                                "tag": "td",
-                                "children": [
-                                  {
-                                    "text": "test"
-                                  }
-                                ]
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><table><tbody><tr><td>test</td></tr></tbody></table></body></html>",
-        "noQuirksBodyHtml": "<table><tbody><tr><td>test</td></tr></tbody></table>"
-      }
-    },
-    {
-      "data": "<frame>test",
-      "errors": [
-        "(1,7): expected-doctype-but-got-start-tag",
-        "(1,7): unexpected-start-tag-ignored"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "test"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>test</body></html>",
-        "noQuirksBodyHtml": "test"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><frameset>test",
-      "errors": [
-        "(1,29): unexpected-char-in-frameset",
-        "(1,29): unexpected-char-in-frameset",
-        "(1,29): unexpected-char-in-frameset",
-        "(1,29): unexpected-char-in-frameset",
-        "(1,29): eof-in-frameset"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "frameset": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "frameset"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><frameset></frameset></html>",
-        "noQuirksBodyHtml": "test"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><frameset> te st",
-      "errors": [
-        "(1,29): unexpected-char-in-frameset",
-        "(1,29): unexpected-char-in-frameset",
-        "(1,29): unexpected-char-in-frameset",
-        "(1,29): unexpected-char-in-frameset",
-        "(1,29): eof-in-frameset"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "frameset": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "frameset",
-                "children": [
-                  {
-                    "text": "  "
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><frameset>  </frameset></html>",
-        "noQuirksBodyHtml": " te st"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><frameset></frameset> te st",
-      "errors": [
-        "(1,29): unexpected-char-after-frameset",
-        "(1,29): unexpected-char-after-frameset",
-        "(1,29): unexpected-char-after-frameset",
-        "(1,29): unexpected-char-after-frameset"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "frameset": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "frameset"
-              },
-              {
-                "text": "  "
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><frameset></frameset>  </html>",
-        "noQuirksBodyHtml": " te st"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><frameset><!DOCTYPE html>",
-      "errors": [
-        "(1,40): unexpected-doctype",
-        "(1,40): eof-in-frameset"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "frameset": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "frameset"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><frameset></frameset></html>",
-        "noQuirksBodyHtml": ""
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><font><p><b>test</font>",
-      "errors": [
-        "(1,38): adoption-agency-1.3",
-        "(1,38): adoption-agency-1.3"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "font": true,
-            "p": true,
-            "b": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "font"
-                  },
-                  {
-                    "tag": "p",
-                    "children": [
-                      {
-                        "tag": "font",
-                        "children": [
-                          {
-                            "tag": "b",
-                            "children": [
-                              {
-                                "text": "test"
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><font></font><p><font><b>test</b></font></p></body></html>",
-        "noQuirksBodyHtml": "<font></font><p><font><b>test</b></font></p>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><dt><div><dd>",
-      "errors": [
-        "(1,28): end-tag-too-early"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "dt": true,
-            "div": true,
-            "dd": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "dt",
-                    "children": [
-                      {
-                        "tag": "div"
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "dd"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><dt><div></div></dt><dd></dd></body></html>",
-        "noQuirksBodyHtml": "<dt><div></div></dt><dd></dd>"
-      }
-    },
-    {
-      "data": "<script></x",
-      "errors": [
-        "(1,8): expected-doctype-but-got-start-tag",
-        "(1,11): expected-named-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "</x",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><script></x</script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script></x</script>"
-      }
-    },
-    {
-      "data": "<table><plaintext><td>",
-      "errors": [
-        "(1,7): expected-doctype-but-got-start-tag",
-        "(1,18): unexpected-start-tag-implies-table-voodoo",
-        "(1,22): foster-parenting-character-in-table",
-        "(1,22): foster-parenting-character-in-table",
-        "(1,22): foster-parenting-character-in-table",
-        "(1,22): foster-parenting-character-in-table",
-        "(1,22): eof-in-table"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "plaintext": true,
-            "table": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "plaintext",
-                    "children": [
-                      {
-                        "text": "<td>",
-                        "no_escape": true
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "table"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><plaintext><td></plaintext><table></table></body></html>",
-        "noQuirksBodyHtml": "<plaintext><td></plaintext><table></table>"
-      }
-    },
-    {
-      "data": "<plaintext></plaintext>",
-      "errors": [
-        "(1,11): expected-doctype-but-got-start-tag",
-        "(1,23): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "plaintext": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "plaintext",
-                    "children": [
-                      {
-                        "text": "</plaintext>",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><plaintext></plaintext></plaintext></body></html>",
-        "noQuirksBodyHtml": "<plaintext></plaintext></plaintext>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><table><tr>TEST",
-      "errors": [
-        "(1,30): foster-parenting-character-in-table",
-        "(1,30): foster-parenting-character-in-table",
-        "(1,30): foster-parenting-character-in-table",
-        "(1,30): foster-parenting-character-in-table",
-        "(1,30): eof-in-table"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true,
-            "tbody": true,
-            "tr": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "TEST"
-                  },
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "tbody",
-                        "children": [
-                          {
-                            "tag": "tr"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body>TEST<table><tbody><tr></tr></tbody></table></body></html>",
-        "noQuirksBodyHtml": "TEST<table><tbody><tr></tr></tbody></table>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><body t1=1><body t2=2><body t3=3 t4=4>",
-      "errors": [
-        "(1,37): unexpected-start-tag",
-        "(1,53): unexpected-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "attrs": [
-                  {
-                    "name": "t1",
-                    "value": "1"
-                  },
-                  {
-                    "name": "t2",
-                    "value": "2"
-                  },
-                  {
-                    "name": "t3",
-                    "value": "3"
-                  },
-                  {
-                    "name": "t4",
-                    "value": "4"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body t1=\"1\" t2=\"2\" t3=\"3\" t4=\"4\"></body></html>",
-        "noQuirksBodyHtml": ""
-      }
-    },
-    {
-      "data": "</b test",
-      "errors": [
-        "(1,8): eof-in-attribute-name",
-        "(1,8): expected-doctype-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body></body></html>",
-        "noQuirksBodyHtml": ""
-      }
-    },
-    {
-      "data": "<!DOCTYPE html></b test<b &=&amp>X",
-      "errors": [
-        "(1,24): invalid-character-in-attribute-name",
-        "(1,32): named-entity-without-semicolon",
-        "(1,33): attributes-in-end-tag",
-        "(1,33): unexpected-end-tag-before-html"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "X"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body>X</body></html>",
-        "noQuirksBodyHtml": "X"
-      }
-    },
-    {
-      "data": "<!doctypehtml><scrIPt type=text/x-foobar;baz>X</SCRipt",
-      "errors": [
-        "(1,9): need-space-after-doctype",
-        "(1,54): expected-named-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "doctype": true,
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "attrs": [
-                      {
-                        "name": "type",
-                        "value": "text/x-foobar;baz"
-                      }
-                    ],
-                    "children": [
-                      {
-                        "text": "X</SCRipt",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head><script type=\"text/x-foobar;baz\">X</SCRipt</script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script type=\"text/x-foobar;baz\">X</SCRipt</script>"
-      }
-    },
-    {
-      "data": "&",
-      "errors": [
-        "(1,1): expected-doctype-but-got-chars"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "escaped": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "&",
-                    "escaped": true
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>&amp;</body></html>",
-        "noQuirksBodyHtml": "&amp;"
-      }
-    },
-    {
-      "data": "&#",
-      "errors": [
-        "(1,2): expected-numeric-entity",
-        "(1,2): expected-doctype-but-got-chars"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "escaped": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "&#",
-                    "escaped": true
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>&amp;#</body></html>",
-        "noQuirksBodyHtml": "&amp;#"
-      }
-    },
-    {
-      "data": "&#X",
-      "errors": [
-        "(1,3): expected-numeric-entity",
-        "(1,3): expected-doctype-but-got-chars"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "escaped": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "&#X",
-                    "escaped": true
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>&amp;#X</body></html>",
-        "noQuirksBodyHtml": "&amp;#X"
-      }
-    },
-    {
-      "data": "&#x",
-      "errors": [
-        "(1,3): expected-numeric-entity",
-        "(1,3): expected-doctype-but-got-chars"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "escaped": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "&#x",
-                    "escaped": true
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>&amp;#x</body></html>",
-        "noQuirksBodyHtml": "&amp;#x"
-      }
-    },
-    {
-      "data": "&#45",
-      "errors": [
-        "(1,4): numeric-entity-without-semicolon",
-        "(1,4): expected-doctype-but-got-chars"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "-"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>-</body></html>",
-        "noQuirksBodyHtml": "-"
-      }
-    },
-    {
-      "data": "&x-test",
-      "errors": [
-        "(1,2): expected-doctype-but-got-chars"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "escaped": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "&x-test",
-                    "escaped": true
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>&amp;x-test</body></html>",
-        "noQuirksBodyHtml": "&amp;x-test"
-      }
-    },
-    {
-      "data": "<!doctypehtml><p><li>",
-      "errors": [
-        "(1,9): need-space-after-doctype"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "p": true,
-            "li": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "p"
-                  },
-                  {
-                    "tag": "li"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><p></p><li></li></body></html>",
-        "noQuirksBodyHtml": "<p></p><li></li>"
-      }
-    },
-    {
-      "data": "<!doctypehtml><p><dt>",
-      "errors": [
-        "(1,9): need-space-after-doctype"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "p": true,
-            "dt": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "p"
-                  },
-                  {
-                    "tag": "dt"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><p></p><dt></dt></body></html>",
-        "noQuirksBodyHtml": "<p></p><dt></dt>"
-      }
-    },
-    {
-      "data": "<!doctypehtml><p><dd>",
-      "errors": [
-        "(1,9): need-space-after-doctype"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "p": true,
-            "dd": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "p"
-                  },
-                  {
-                    "tag": "dd"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><p></p><dd></dd></body></html>",
-        "noQuirksBodyHtml": "<p></p><dd></dd>"
-      }
-    },
-    {
-      "data": "<!doctypehtml><p><form>",
-      "errors": [
-        "(1,9): need-space-after-doctype",
-        "(1,23): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "p": true,
-            "form": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "p"
-                  },
-                  {
-                    "tag": "form"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><p></p><form></form></body></html>",
-        "noQuirksBodyHtml": "<p></p><form></form>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><p></P>X",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "p": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "p"
-                  },
-                  {
-                    "text": "X"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><p></p>X</body></html>",
-        "noQuirksBodyHtml": "<p></p>X"
-      }
-    },
-    {
-      "data": "&AMP",
-      "errors": [
-        "(1,4): named-entity-without-semicolon",
-        "(1,4): expected-doctype-but-got-chars"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "escaped": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "&",
-                    "escaped": true
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>&amp;</body></html>",
-        "noQuirksBodyHtml": "&amp;"
-      }
-    },
-    {
-      "data": "&AMp;",
-      "errors": [
-        "(1,3): expected-named-entity",
-        "(1,3): expected-doctype-but-got-chars"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "escaped": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "&AMp;",
-                    "escaped": true
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>&amp;AMp;</body></html>",
-        "noQuirksBodyHtml": "&amp;AMp;"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><html><head></head><body><thisISasillyTESTelementNameToMakeSureCrazyTagNamesArePARSEDcorrectLY>",
-      "errors": [
-        "(1,110): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "thisisasillytestelementnametomakesurecrazytagnamesareparsedcorrectly": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "thisisasillytestelementnametomakesurecrazytagnamesareparsedcorrectly"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><thisisasillytestelementnametomakesurecrazytagnamesareparsedcorrectly></thisisasillytestelementnametomakesurecrazytagnamesareparsedcorrectly></body></html>",
-        "noQuirksBodyHtml": "<thisisasillytestelementnametomakesurecrazytagnamesareparsedcorrectly></thisisasillytestelementnametomakesurecrazytagnamesareparsedcorrectly>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html>X</body>X",
-      "errors": [
-        "(1,24): unexpected-char-after-body"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "XX"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body>XX</body></html>",
-        "noQuirksBodyHtml": "XX"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><!-- X",
-      "errors": [
-        "(1,21): eof-in-comment"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "doctype": true,
-          "comment": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "comment": " X"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><!-- X--><html><head></head><body></body></html>",
-        "noQuirksBodyHtml": "<!-- X-->"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><table><caption>test TEST</caption><td>test",
-      "errors": [
-        "(1,54): unexpected-cell-in-table-body",
-        "(1,58): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true,
-            "caption": true,
-            "tbody": true,
-            "tr": true,
-            "td": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "caption",
-                        "children": [
-                          {
-                            "text": "test TEST"
-                          }
-                        ]
-                      },
-                      {
-                        "tag": "tbody",
-                        "children": [
-                          {
-                            "tag": "tr",
-                            "children": [
-                              {
-                                "tag": "td",
-                                "children": [
-                                  {
-                                    "text": "test"
-                                  }
-                                ]
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><table><caption>test TEST</caption><tbody><tr><td>test</td></tr></tbody></table></body></html>",
-        "noQuirksBodyHtml": "<table><caption>test TEST</caption><tbody><tr><td>test</td></tr></tbody></table>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><select><option><optgroup>",
-      "errors": [
-        "(1,41): eof-in-select"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "select": true,
-            "option": true,
-            "optgroup": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "select",
-                    "children": [
-                      {
-                        "tag": "option"
-                      },
-                      {
-                        "tag": "optgroup"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><select><option></option><optgroup></optgroup></select></body></html>",
-        "noQuirksBodyHtml": "<select><option></option><optgroup></optgroup></select>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><select><optgroup><option></optgroup><option><select><option>",
-      "errors": [
-        "(1,68): unexpected-select-in-select"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "select": true,
-            "optgroup": true,
-            "option": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "select",
-                    "children": [
-                      {
-                        "tag": "optgroup",
-                        "children": [
-                          {
-                            "tag": "option"
-                          }
-                        ]
-                      },
-                      {
-                        "tag": "option"
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "option"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><select><optgroup><option></option></optgroup><option></option></select><option></option></body></html>",
-        "noQuirksBodyHtml": "<select><optgroup><option></option></optgroup><option></option></select><option></option>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><select><optgroup><option><optgroup>",
-      "errors": [
-        "(1,51): eof-in-select"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "select": true,
-            "optgroup": true,
-            "option": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "select",
-                    "children": [
-                      {
-                        "tag": "optgroup",
-                        "children": [
-                          {
-                            "tag": "option"
-                          }
-                        ]
-                      },
-                      {
-                        "tag": "optgroup"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><select><optgroup><option></option></optgroup><optgroup></optgroup></select></body></html>",
-        "noQuirksBodyHtml": "<select><optgroup><option></option></optgroup><optgroup></optgroup></select>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><datalist><option>foo</datalist>bar",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "datalist": true,
-            "option": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "datalist",
-                    "children": [
-                      {
-                        "tag": "option",
-                        "children": [
-                          {
-                            "text": "foo"
-                          }
-                        ]
-                      }
-                    ]
-                  },
-                  {
-                    "text": "bar"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><datalist><option>foo</option></datalist>bar</body></html>",
-        "noQuirksBodyHtml": "<datalist><option>foo</option></datalist>bar"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><font><input><input></font>",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "font": true,
-            "input": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "font",
-                    "children": [
-                      {
-                        "tag": "input"
-                      },
-                      {
-                        "tag": "input"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><font><input><input></font></body></html>",
-        "noQuirksBodyHtml": "<font><input><input></font>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><!-- XXX - XXX -->",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "doctype": true,
-          "comment": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "comment": " XXX - XXX "
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><!-- XXX - XXX --><html><head></head><body></body></html>",
-        "noQuirksBodyHtml": "<!-- XXX - XXX -->"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><!-- XXX - XXX",
-      "errors": [
-        "(1,29): eof-in-comment"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "doctype": true,
-          "comment": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "comment": " XXX - XXX"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><!-- XXX - XXX--><html><head></head><body></body></html>",
-        "noQuirksBodyHtml": "<!-- XXX - XXX-->"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><!-- XXX - XXX - XXX -->",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "doctype": true,
-          "comment": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "comment": " XXX - XXX - XXX "
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><!-- XXX - XXX - XXX --><html><head></head><body></body></html>",
-        "noQuirksBodyHtml": "<!-- XXX - XXX - XXX -->"
-      }
-    },
-    {
-      "data": "test\ntest",
-      "errors": [
-        "(2,4): expected-doctype-but-got-chars"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "test\ntest"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>test\ntest</body></html>",
-        "noQuirksBodyHtml": "test\ntest"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><body><title>test</body></title>",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "title": true
-          },
-          "doctype": true,
-          "escaped": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "title",
-                    "children": [
-                      {
-                        "text": "test</body>",
-                        "escaped": true
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><title>test&lt;/body&gt;</title></body></html>",
-        "noQuirksBodyHtml": "<title>test&lt;/body&gt;</title>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><body><title>X</title><meta name=z><link rel=foo><style>\nx { content:\"</style\" } </style>",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "title": true,
-            "meta": true,
-            "link": true,
-            "style": true
-          },
-          "doctype": true,
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "title",
-                    "children": [
-                      {
-                        "text": "X"
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "meta",
-                    "attrs": [
-                      {
-                        "name": "name",
-                        "value": "z"
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "link",
-                    "attrs": [
-                      {
-                        "name": "rel",
-                        "value": "foo"
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "style",
-                    "children": [
-                      {
-                        "text": "\nx { content:\"</style\" } ",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><title>X</title><meta name=\"z\"><link rel=\"foo\"><style>\nx { content:\"</style\" } </style></body></html>",
-        "noQuirksBodyHtml": "<title>X</title><meta name=\"z\"><link rel=\"foo\"><style>\nx { content:\"</style\" } </style>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><select><optgroup></optgroup></select>",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "select": true,
-            "optgroup": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "select",
-                    "children": [
-                      {
-                        "tag": "optgroup"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><select><optgroup></optgroup></select></body></html>",
-        "noQuirksBodyHtml": "<select><optgroup></optgroup></select>"
-      }
-    },
-    {
-      "data": " \n ",
-      "errors": [
-        "(2,1): expected-doctype-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body></body></html>",
-        "noQuirksBodyHtml": " \n "
-      }
-    },
-    {
-      "data": "<!DOCTYPE html>  <html>",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body></body></html>",
-        "noQuirksBodyHtml": "  "
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><script>\n</script>  <title>x</title>  </head>",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "title": true,
-            "body": true
-          },
-          "doctype": true,
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "\n",
-                        "no_escape": true
-                      }
-                    ]
-                  },
-                  {
-                    "text": "  "
-                  },
-                  {
-                    "tag": "title",
-                    "children": [
-                      {
-                        "text": "x"
-                      }
-                    ]
-                  },
-                  {
-                    "text": "  "
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head><script>\n</script>  <title>x</title>  </head><body></body></html>",
-        "noQuirksBodyHtml": "<script>\n</script>  <title>x</title>  "
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><html><body><html id=x>",
-      "errors": [
-        "(1,38): non-html-root"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "attrs": [
-              {
-                "name": "id",
-                "value": "x"
-              }
-            ],
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html id=\"x\"><head></head><body></body></html>",
-        "noQuirksBodyHtml": ""
-      }
-    },
-    {
-      "data": "<!DOCTYPE html>X</body><html id=\"x\">",
-      "errors": [
-        "(1,36): non-html-root"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "attrs": [
-              {
-                "name": "id",
-                "value": "x"
-              }
-            ],
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "X"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html id=\"x\"><head></head><body>X</body></html>",
-        "noQuirksBodyHtml": "X"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><head><html id=x>",
-      "errors": [
-        "(1,32): non-html-root"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "attrs": [
-              {
-                "name": "id",
-                "value": "x"
-              }
-            ],
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html id=\"x\"><head></head><body></body></html>",
-        "noQuirksBodyHtml": ""
-      }
-    },
-    {
-      "data": "<!DOCTYPE html>X</html>X",
-      "errors": [
-        "(1,24): expected-eof-but-got-char"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "XX"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body>XX</body></html>",
-        "noQuirksBodyHtml": "XX"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html>X</html> ",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "X "
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body>X </body></html>",
-        "noQuirksBodyHtml": "X "
-      }
-    },
-    {
-      "data": "<!DOCTYPE html>X</html><p>X",
-      "errors": [
-        "(1,26): expected-eof-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "p": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "X"
-                  },
-                  {
-                    "tag": "p",
-                    "children": [
-                      {
-                        "text": "X"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body>X<p>X</p></body></html>",
-        "noQuirksBodyHtml": "X<p>X</p>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html>X<p/x/y/z>",
-      "errors": [
-        "(1,19): unexpected-character-after-solidus-in-tag",
-        "(1,21): unexpected-character-after-solidus-in-tag",
-        "(1,23): unexpected-character-after-solidus-in-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "p": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "X"
-                  },
-                  {
-                    "tag": "p",
-                    "attrs": [
-                      {
-                        "name": "x",
-                        "value": ""
-                      },
-                      {
-                        "name": "y",
-                        "value": ""
-                      },
-                      {
-                        "name": "z",
-                        "value": ""
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body>X<p x=\"\" y=\"\" z=\"\"></p></body></html>",
-        "noQuirksBodyHtml": "X<p x=\"\" y=\"\" z=\"\"></p>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><!--x--",
-      "errors": [
-        "(1,22): eof-in-comment-double-dash"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "doctype": true,
-          "comment": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "comment": "x"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><!--x--><html><head></head><body></body></html>",
-        "noQuirksBodyHtml": "<!--x-->"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><table><tr><td></p></table>",
-      "errors": [
-        "(1,34): unexpected-end-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true,
-            "tbody": true,
-            "tr": true,
-            "td": true,
-            "p": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "tbody",
-                        "children": [
-                          {
-                            "tag": "tr",
-                            "children": [
-                              {
-                                "tag": "td",
-                                "children": [
-                                  {
-                                    "tag": "p"
-                                  }
-                                ]
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><table><tbody><tr><td><p></p></td></tr></tbody></table></body></html>",
-        "noQuirksBodyHtml": "<table><tbody><tr><td><p></p></td></tr></tbody></table>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE <!DOCTYPE HTML>><!--<!--x-->-->",
-      "errors": [
-        "(1,20): expected-space-or-right-bracket-in-doctype",
-        "(1,25): unknown-doctype",
-        "(1,35): unexpected-char-in-comment"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "doctype": true,
-          "escaped": true,
-          "comment": true
-        },
-        "tree": [
-          {
-            "doctype": "<!doctype"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": ">",
-                    "escaped": true
-                  },
-                  {
-                    "comment": "<!--x"
-                  },
-                  {
-                    "text": "-->",
-                    "escaped": true
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE <!doctype><html><head></head><body>&gt;<!--<!--x-->--&gt;</body></html>",
-        "noQuirksBodyHtml": "&gt;<!--<!--x-->--&gt;"
-      }
-    },
-    {
-      "data": "<!doctype html><div><form></form><div></div></div>",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "div": true,
-            "form": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "div",
-                    "children": [
-                      {
-                        "tag": "form"
-                      },
-                      {
-                        "tag": "div"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><div><form></form><div></div></div></body></html>",
-        "noQuirksBodyHtml": "<div><form></form><div></div></div>"
-      }
-    }
-  ],
-  "tests20.dat": [
-    {
-      "data": "<!doctype html><p><button><button>",
-      "errors": [
-        "(1,34): unexpected-start-tag-implies-end-tag",
-        "(1,34): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "p": true,
-            "button": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "p",
-                    "children": [
-                      {
-                        "tag": "button"
-                      },
-                      {
-                        "tag": "button"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><p><button></button><button></button></p></body></html>",
-        "noQuirksBodyHtml": "<p><button></button><button></button></p>"
-      }
-    },
-    {
-      "data": "<!doctype html><p><button><address>",
-      "errors": [
-        "(1,35): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "p": true,
-            "button": true,
-            "address": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "p",
-                    "children": [
-                      {
-                        "tag": "button",
-                        "children": [
-                          {
-                            "tag": "address"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><p><button><address></address></button></p></body></html>",
-        "noQuirksBodyHtml": "<p><button><address></address></button></p>"
-      }
-    },
-    {
-      "data": "<!doctype html><p><button><blockquote>",
-      "errors": [
-        "(1,38): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "p": true,
-            "button": true,
-            "blockquote": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "p",
-                    "children": [
-                      {
-                        "tag": "button",
-                        "children": [
-                          {
-                            "tag": "blockquote"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><p><button><blockquote></blockquote></button></p></body></html>",
-        "noQuirksBodyHtml": "<p><button><blockquote></blockquote></button></p>"
-      }
-    },
-    {
-      "data": "<!doctype html><p><button><menu>",
-      "errors": [
-        "(1,32): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "p": true,
-            "button": true,
-            "menu": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "p",
-                    "children": [
-                      {
-                        "tag": "button",
-                        "children": [
-                          {
-                            "tag": "menu"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><p><button><menu></menu></button></p></body></html>",
-        "noQuirksBodyHtml": "<p><button><menu></menu></button></p>"
-      }
-    },
-    {
-      "data": "<!doctype html><p><button><p>",
-      "errors": [
-        "(1,29): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "p": true,
-            "button": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "p",
-                    "children": [
-                      {
-                        "tag": "button",
-                        "children": [
-                          {
-                            "tag": "p"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><p><button><p></p></button></p></body></html>",
-        "noQuirksBodyHtml": "<p><button><p></p></button></p>"
-      }
-    },
-    {
-      "data": "<!doctype html><p><button><ul>",
-      "errors": [
-        "(1,30): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "p": true,
-            "button": true,
-            "ul": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "p",
-                    "children": [
-                      {
-                        "tag": "button",
-                        "children": [
-                          {
-                            "tag": "ul"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><p><button><ul></ul></button></p></body></html>",
-        "noQuirksBodyHtml": "<p><button><ul></ul></button></p>"
-      }
-    },
-    {
-      "data": "<!doctype html><p><button><h1>",
-      "errors": [
-        "(1,30): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "p": true,
-            "button": true,
-            "h1": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "p",
-                    "children": [
-                      {
-                        "tag": "button",
-                        "children": [
-                          {
-                            "tag": "h1"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><p><button><h1></h1></button></p></body></html>",
-        "noQuirksBodyHtml": "<p><button><h1></h1></button></p>"
-      }
-    },
-    {
-      "data": "<!doctype html><p><button><h6>",
-      "errors": [
-        "(1,30): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "p": true,
-            "button": true,
-            "h6": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "p",
-                    "children": [
-                      {
-                        "tag": "button",
-                        "children": [
-                          {
-                            "tag": "h6"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><p><button><h6></h6></button></p></body></html>",
-        "noQuirksBodyHtml": "<p><button><h6></h6></button></p>"
-      }
-    },
-    {
-      "data": "<!doctype html><p><button><listing>",
-      "errors": [
-        "(1,35): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "p": true,
-            "button": true,
-            "listing": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "p",
-                    "children": [
-                      {
-                        "tag": "button",
-                        "children": [
-                          {
-                            "tag": "listing"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><p><button><listing></listing></button></p></body></html>",
-        "noQuirksBodyHtml": "<p><button><listing></listing></button></p>"
-      }
-    },
-    {
-      "data": "<!doctype html><p><button><pre>",
-      "errors": [
-        "(1,31): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "p": true,
-            "button": true,
-            "pre": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "p",
-                    "children": [
-                      {
-                        "tag": "button",
-                        "children": [
-                          {
-                            "tag": "pre"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><p><button><pre></pre></button></p></body></html>",
-        "noQuirksBodyHtml": "<p><button><pre></pre></button></p>"
-      }
-    },
-    {
-      "data": "<!doctype html><p><button><form>",
-      "errors": [
-        "(1,32): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "p": true,
-            "button": true,
-            "form": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "p",
-                    "children": [
-                      {
-                        "tag": "button",
-                        "children": [
-                          {
-                            "tag": "form"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><p><button><form></form></button></p></body></html>",
-        "noQuirksBodyHtml": "<p><button><form></form></button></p>"
-      }
-    },
-    {
-      "data": "<!doctype html><p><button><li>",
-      "errors": [
-        "(1,30): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "p": true,
-            "button": true,
-            "li": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "p",
-                    "children": [
-                      {
-                        "tag": "button",
-                        "children": [
-                          {
-                            "tag": "li"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><p><button><li></li></button></p></body></html>",
-        "noQuirksBodyHtml": "<p><button><li></li></button></p>"
-      }
-    },
-    {
-      "data": "<!doctype html><p><button><dd>",
-      "errors": [
-        "(1,30): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "p": true,
-            "button": true,
-            "dd": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "p",
-                    "children": [
-                      {
-                        "tag": "button",
-                        "children": [
-                          {
-                            "tag": "dd"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><p><button><dd></dd></button></p></body></html>",
-        "noQuirksBodyHtml": "<p><button><dd></dd></button></p>"
-      }
-    },
-    {
-      "data": "<!doctype html><p><button><dt>",
-      "errors": [
-        "(1,30): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "p": true,
-            "button": true,
-            "dt": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "p",
-                    "children": [
-                      {
-                        "tag": "button",
-                        "children": [
-                          {
-                            "tag": "dt"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><p><button><dt></dt></button></p></body></html>",
-        "noQuirksBodyHtml": "<p><button><dt></dt></button></p>"
-      }
-    },
-    {
-      "data": "<!doctype html><p><button><plaintext>",
-      "errors": [
-        "(1,37): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "p": true,
-            "button": true,
-            "plaintext": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "p",
-                    "children": [
-                      {
-                        "tag": "button",
-                        "children": [
-                          {
-                            "tag": "plaintext"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><p><button><plaintext></plaintext></button></p></body></html>",
-        "noQuirksBodyHtml": "<p><button><plaintext></plaintext></button></p>"
-      }
-    },
-    {
-      "data": "<!doctype html><p><button><table>",
-      "errors": [
-        "(1,33): eof-in-table"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "p": true,
-            "button": true,
-            "table": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "p",
-                    "children": [
-                      {
-                        "tag": "button",
-                        "children": [
-                          {
-                            "tag": "table"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><p><button><table></table></button></p></body></html>",
-        "noQuirksBodyHtml": "<p><button><table></table></button></p>"
-      }
-    },
-    {
-      "data": "<!doctype html><p><button><hr>",
-      "errors": [
-        "(1,30): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "p": true,
-            "button": true,
-            "hr": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "p",
-                    "children": [
-                      {
-                        "tag": "button",
-                        "children": [
-                          {
-                            "tag": "hr"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><p><button><hr></button></p></body></html>",
-        "noQuirksBodyHtml": "<p><button><hr></button></p>"
-      }
-    },
-    {
-      "data": "<!doctype html><p><button><xmp>",
-      "errors": [
-        "(1,31): expected-named-closing-tag-but-got-eof",
-        "(1,31): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "p": true,
-            "button": true,
-            "xmp": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "p",
-                    "children": [
-                      {
-                        "tag": "button",
-                        "children": [
-                          {
-                            "tag": "xmp"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><p><button><xmp></xmp></button></p></body></html>",
-        "noQuirksBodyHtml": "<p><button><xmp></xmp></button></p>"
-      }
-    },
-    {
-      "data": "<!doctype html><p><button></p>",
-      "errors": [
-        "(1,30): unexpected-end-tag",
-        "(1,30): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "p": true,
-            "button": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "p",
-                    "children": [
-                      {
-                        "tag": "button",
-                        "children": [
-                          {
-                            "tag": "p"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><p><button><p></p></button></p></body></html>",
-        "noQuirksBodyHtml": "<p><button><p></p></button></p>"
-      }
-    },
-    {
-      "data": "<!doctype html><address><button></address>a",
-      "errors": [
-        "(1,42): end-tag-too-early"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "address": true,
-            "button": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "address",
-                    "children": [
-                      {
-                        "tag": "button"
-                      }
-                    ]
-                  },
-                  {
-                    "text": "a"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><address><button></button></address>a</body></html>",
-        "noQuirksBodyHtml": "<address><button></button></address>a"
-      }
-    },
-    {
-      "data": "<!doctype html><address><button></address>a",
-      "errors": [
-        "(1,42): end-tag-too-early"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "address": true,
-            "button": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "address",
-                    "children": [
-                      {
-                        "tag": "button"
-                      }
-                    ]
-                  },
-                  {
-                    "text": "a"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><address><button></button></address>a</body></html>",
-        "noQuirksBodyHtml": "<address><button></button></address>a"
-      }
-    },
-    {
-      "data": "<p><table></p>",
-      "errors": [
-        "(1,3): expected-doctype-but-got-start-tag",
-        "(1,14): unexpected-end-tag-implies-table-voodoo",
-        "(1,14): unexpected-end-tag",
-        "(1,14): eof-in-table"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "p": true,
-            "table": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "p",
-                    "children": [
-                      {
-                        "tag": "p"
-                      },
-                      {
-                        "tag": "table"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><p><p></p><table></table></p></body></html>",
-        "noQuirksBodyHtml": "<p></p><p></p><table></table>"
-      }
-    },
-    {
-      "data": "<!doctype html><svg>",
-      "errors": [
-        "(1,20): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "svg svg": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "svg",
-                    "ns": "http://www.w3.org/2000/svg"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><svg></svg></body></html>",
-        "noQuirksBodyHtml": "<svg></svg>"
-      }
-    },
-    {
-      "data": "<!doctype html><p><figcaption>",
-      "errors": [
-        "(1,30): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "p": true,
-            "figcaption": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "p"
-                  },
-                  {
-                    "tag": "figcaption"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><p></p><figcaption></figcaption></body></html>",
-        "noQuirksBodyHtml": "<p></p><figcaption></figcaption>"
-      }
-    },
-    {
-      "data": "<!doctype html><p><summary>",
-      "errors": [
-        "(1,27): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "p": true,
-            "summary": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "p"
-                  },
-                  {
-                    "tag": "summary"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><p></p><summary></summary></body></html>",
-        "noQuirksBodyHtml": "<p></p><summary></summary>"
-      }
-    },
-    {
-      "data": "<!doctype html><form><table><form>",
-      "errors": [
-        "(1,34): unexpected-form-in-table",
-        "(1,34): eof-in-table"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "form": true,
-            "table": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "form",
-                    "children": [
-                      {
-                        "tag": "table"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><form><table></table></form></body></html>",
-        "noQuirksBodyHtml": "<form><table></table></form>"
-      }
-    },
-    {
-      "data": "<!doctype html><table><form><form>",
-      "errors": [
-        "(1,28): unexpected-form-in-table",
-        "(1,34): unexpected-form-in-table",
-        "(1,34): eof-in-table"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true,
-            "form": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "form"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><table><form></form></table></body></html>",
-        "noQuirksBodyHtml": "<table><form></form></table>"
-      }
-    },
-    {
-      "data": "<!doctype html><table><form></table><form>",
-      "errors": [
-        "(1,28): unexpected-form-in-table",
-        "(1,42): unexpected-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true,
-            "form": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "form"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><table><form></form></table></body></html>",
-        "noQuirksBodyHtml": "<table><form></form></table>"
-      }
-    },
-    {
-      "data": "<!doctype html><svg><foreignObject><p>",
-      "errors": [
-        "(1,38): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "svg svg": true,
-            "svg foreignObject": true,
-            "p": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "svg",
-                    "ns": "http://www.w3.org/2000/svg",
-                    "children": [
-                      {
-                        "tag": "foreignObject",
-                        "ns": "http://www.w3.org/2000/svg",
-                        "children": [
-                          {
-                            "tag": "p"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><svg><foreignObject><p></p></foreignObject></svg></body></html>",
-        "noQuirksBodyHtml": "<svg><foreignObject><p></p></foreignObject></svg>"
-      }
-    },
-    {
-      "data": "<!doctype html><svg><title>abc",
-      "errors": [
-        "(1,30): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "svg svg": true,
-            "svg title": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "svg",
-                    "ns": "http://www.w3.org/2000/svg",
-                    "children": [
-                      {
-                        "tag": "title",
-                        "ns": "http://www.w3.org/2000/svg",
-                        "children": [
-                          {
-                            "text": "abc"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><svg><title>abc</title></svg></body></html>",
-        "noQuirksBodyHtml": "<svg><title>abc</title></svg>"
-      }
-    },
-    {
-      "data": "<option><span><option>",
-      "errors": [
-        "(1,8): expected-doctype-but-got-start-tag",
-        "(1,22): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "option": true,
-            "span": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "option",
-                    "children": [
-                      {
-                        "tag": "span",
-                        "children": [
-                          {
-                            "tag": "option"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><option><span><option></option></span></option></body></html>",
-        "noQuirksBodyHtml": "<option><span><option></option></span></option>"
-      }
-    },
-    {
-      "data": "<option><option>",
-      "errors": [
-        "(1,8): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "option": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "option"
-                  },
-                  {
-                    "tag": "option"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><option></option><option></option></body></html>",
-        "noQuirksBodyHtml": "<option></option><option></option>"
-      }
-    },
-    {
-      "data": "<math><annotation-xml><div>",
-      "errors": [
-        "(1,6): expected-doctype-but-got-start-tag",
-        "(1,27): unexpected-html-element-in-foreign-content",
-        "(1,27): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "math math": true,
-            "math annotation-xml": true,
-            "div": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "math",
-                    "ns": "http://www.w3.org/1998/Math/MathML",
-                    "children": [
-                      {
-                        "tag": "annotation-xml",
-                        "ns": "http://www.w3.org/1998/Math/MathML"
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "div"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><math><annotation-xml></annotation-xml></math><div></div></body></html>",
-        "noQuirksBodyHtml": "<math><annotation-xml><div></div></annotation-xml></math>"
-      }
-    },
-    {
-      "data": "<math><annotation-xml encoding=\"application/svg+xml\"><div>",
-      "errors": [
-        "(1,6): expected-doctype-but-got-start-tag",
-        "(1,58): unexpected-html-element-in-foreign-content",
-        "(1,58): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "math math": true,
-            "math annotation-xml": true,
-            "div": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "math",
-                    "ns": "http://www.w3.org/1998/Math/MathML",
-                    "children": [
-                      {
-                        "tag": "annotation-xml",
-                        "ns": "http://www.w3.org/1998/Math/MathML",
-                        "attrs": [
-                          {
-                            "name": "encoding",
-                            "value": "application/svg+xml"
-                          }
-                        ]
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "div"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><math><annotation-xml encoding=\"application/svg+xml\"></annotation-xml></math><div></div></body></html>",
-        "noQuirksBodyHtml": "<math><annotation-xml encoding=\"application/svg+xml\"><div></div></annotation-xml></math>"
-      }
-    },
-    {
-      "data": "<math><annotation-xml encoding=\"application/xhtml+xml\"><div>",
-      "errors": [
-        "(1,6): expected-doctype-but-got-start-tag",
-        "(1,60): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "math math": true,
-            "math annotation-xml": true,
-            "div": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "math",
-                    "ns": "http://www.w3.org/1998/Math/MathML",
-                    "children": [
-                      {
-                        "tag": "annotation-xml",
-                        "ns": "http://www.w3.org/1998/Math/MathML",
-                        "attrs": [
-                          {
-                            "name": "encoding",
-                            "value": "application/xhtml+xml"
-                          }
-                        ],
-                        "children": [
-                          {
-                            "tag": "div"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><math><annotation-xml encoding=\"application/xhtml+xml\"><div></div></annotation-xml></math></body></html>",
-        "noQuirksBodyHtml": "<math><annotation-xml encoding=\"application/xhtml+xml\"><div></div></annotation-xml></math>"
-      }
-    },
-    {
-      "data": "<math><annotation-xml encoding=\"aPPlication/xhtmL+xMl\"><div>",
-      "errors": [
-        "(1,6): expected-doctype-but-got-start-tag",
-        "(1,60): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "math math": true,
-            "math annotation-xml": true,
-            "div": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "math",
-                    "ns": "http://www.w3.org/1998/Math/MathML",
-                    "children": [
-                      {
-                        "tag": "annotation-xml",
-                        "ns": "http://www.w3.org/1998/Math/MathML",
-                        "attrs": [
-                          {
-                            "name": "encoding",
-                            "value": "aPPlication/xhtmL+xMl"
-                          }
-                        ],
-                        "children": [
-                          {
-                            "tag": "div"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><math><annotation-xml encoding=\"aPPlication/xhtmL+xMl\"><div></div></annotation-xml></math></body></html>",
-        "noQuirksBodyHtml": "<math><annotation-xml encoding=\"aPPlication/xhtmL+xMl\"><div></div></annotation-xml></math>"
-      }
-    },
-    {
-      "data": "<math><annotation-xml encoding=\"text/html\"><div>",
-      "errors": [
-        "(1,6): expected-doctype-but-got-start-tag",
-        "(1,48): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "math math": true,
-            "math annotation-xml": true,
-            "div": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "math",
-                    "ns": "http://www.w3.org/1998/Math/MathML",
-                    "children": [
-                      {
-                        "tag": "annotation-xml",
-                        "ns": "http://www.w3.org/1998/Math/MathML",
-                        "attrs": [
-                          {
-                            "name": "encoding",
-                            "value": "text/html"
-                          }
-                        ],
-                        "children": [
-                          {
-                            "tag": "div"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><math><annotation-xml encoding=\"text/html\"><div></div></annotation-xml></math></body></html>",
-        "noQuirksBodyHtml": "<math><annotation-xml encoding=\"text/html\"><div></div></annotation-xml></math>"
-      }
-    },
-    {
-      "data": "<math><annotation-xml encoding=\"Text/htmL\"><div>",
-      "errors": [
-        "(1,6): expected-doctype-but-got-start-tag",
-        "(1,48): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "math math": true,
-            "math annotation-xml": true,
-            "div": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "math",
-                    "ns": "http://www.w3.org/1998/Math/MathML",
-                    "children": [
-                      {
-                        "tag": "annotation-xml",
-                        "ns": "http://www.w3.org/1998/Math/MathML",
-                        "attrs": [
-                          {
-                            "name": "encoding",
-                            "value": "Text/htmL"
-                          }
-                        ],
-                        "children": [
-                          {
-                            "tag": "div"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><math><annotation-xml encoding=\"Text/htmL\"><div></div></annotation-xml></math></body></html>",
-        "noQuirksBodyHtml": "<math><annotation-xml encoding=\"Text/htmL\"><div></div></annotation-xml></math>"
-      }
-    },
-    {
-      "data": "<math><annotation-xml encoding=\" text/html \"><div>",
-      "errors": [
-        "(1,6): expected-doctype-but-got-start-tag",
-        "(1,50): unexpected-html-element-in-foreign-content",
-        "(1,50): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "math math": true,
-            "math annotation-xml": true,
-            "div": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "math",
-                    "ns": "http://www.w3.org/1998/Math/MathML",
-                    "children": [
-                      {
-                        "tag": "annotation-xml",
-                        "ns": "http://www.w3.org/1998/Math/MathML",
-                        "attrs": [
-                          {
-                            "name": "encoding",
-                            "value": " text/html "
-                          }
-                        ]
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "div"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><math><annotation-xml encoding=\" text/html \"></annotation-xml></math><div></div></body></html>",
-        "noQuirksBodyHtml": "<math><annotation-xml encoding=\" text/html \"><div></div></annotation-xml></math>"
-      }
-    },
-    {
-      "data": "<math><annotation-xml> </annotation-xml>",
-      "errors": [
-        "(1,6): expected-doctype-but-got-start-tag",
-        "(1,40): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "math math": true,
-            "math annotation-xml": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "math",
-                    "ns": "http://www.w3.org/1998/Math/MathML",
-                    "children": [
-                      {
-                        "tag": "annotation-xml",
-                        "ns": "http://www.w3.org/1998/Math/MathML",
-                        "children": [
-                          {
-                            "text": " "
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><math><annotation-xml> </annotation-xml></math></body></html>",
-        "noQuirksBodyHtml": "<math><annotation-xml> </annotation-xml></math>"
-      }
-    },
-    {
-      "data": "<math><annotation-xml>c</annotation-xml>",
-      "errors": [
-        "(1,6): expected-doctype-but-got-start-tag",
-        "(1,40): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "math math": true,
-            "math annotation-xml": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "math",
-                    "ns": "http://www.w3.org/1998/Math/MathML",
-                    "children": [
-                      {
-                        "tag": "annotation-xml",
-                        "ns": "http://www.w3.org/1998/Math/MathML",
-                        "children": [
-                          {
-                            "text": "c"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><math><annotation-xml>c</annotation-xml></math></body></html>",
-        "noQuirksBodyHtml": "<math><annotation-xml>c</annotation-xml></math>"
-      }
-    },
-    {
-      "data": "<math><annotation-xml><!--foo-->",
-      "errors": [
-        "(1,6): expected-doctype-but-got-start-tag",
-        "(1,32): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "math math": true,
-            "math annotation-xml": true
-          },
-          "comment": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "math",
-                    "ns": "http://www.w3.org/1998/Math/MathML",
-                    "children": [
-                      {
-                        "tag": "annotation-xml",
-                        "ns": "http://www.w3.org/1998/Math/MathML",
-                        "children": [
-                          {
-                            "comment": "foo"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><math><annotation-xml><!--foo--></annotation-xml></math></body></html>",
-        "noQuirksBodyHtml": "<math><annotation-xml><!--foo--></annotation-xml></math>"
-      }
-    },
-    {
-      "data": "<math><annotation-xml></svg>x",
-      "errors": [
-        "(1,6): expected-doctype-but-got-start-tag",
-        "(1,28): unexpected-end-tag",
-        "(1,29): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "math math": true,
-            "math annotation-xml": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "math",
-                    "ns": "http://www.w3.org/1998/Math/MathML",
-                    "children": [
-                      {
-                        "tag": "annotation-xml",
-                        "ns": "http://www.w3.org/1998/Math/MathML",
-                        "children": [
-                          {
-                            "text": "x"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><math><annotation-xml>x</annotation-xml></math></body></html>",
-        "noQuirksBodyHtml": "<math><annotation-xml>x</annotation-xml></math>"
-      }
-    },
-    {
-      "data": "<math><annotation-xml><svg>x",
-      "errors": [
-        "(1,6): expected-doctype-but-got-start-tag",
-        "(1,28): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "math math": true,
-            "math annotation-xml": true,
-            "svg svg": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "math",
-                    "ns": "http://www.w3.org/1998/Math/MathML",
-                    "children": [
-                      {
-                        "tag": "annotation-xml",
-                        "ns": "http://www.w3.org/1998/Math/MathML",
-                        "children": [
-                          {
-                            "tag": "svg",
-                            "ns": "http://www.w3.org/2000/svg",
-                            "children": [
-                              {
-                                "text": "x"
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><math><annotation-xml><svg>x</svg></annotation-xml></math></body></html>",
-        "noQuirksBodyHtml": "<math><annotation-xml><svg>x</svg></annotation-xml></math>"
-      }
-    }
-  ],
-  "tests21.dat": [
-    {
-      "data": "<svg><![CDATA[foo]]>",
-      "errors": [
-        "(1,5): expected-doctype-but-got-start-tag",
-        "(1,20): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "svg svg": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "svg",
-                    "ns": "http://www.w3.org/2000/svg",
-                    "children": [
-                      {
-                        "text": "foo"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><svg>foo</svg></body></html>",
-        "noQuirksBodyHtml": "<svg>foo</svg>"
-      }
-    },
-    {
-      "data": "<math><![CDATA[foo]]>",
-      "errors": [
-        "(1,6): expected-doctype-but-got-start-tag",
-        "(1,21): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "math math": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "math",
-                    "ns": "http://www.w3.org/1998/Math/MathML",
-                    "children": [
-                      {
-                        "text": "foo"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><math>foo</math></body></html>",
-        "noQuirksBodyHtml": "<math>foo</math>"
-      }
-    },
-    {
-      "data": "<div><![CDATA[foo]]>",
-      "errors": [
-        "(1,5): expected-doctype-but-got-start-tag",
-        "(1,7): expected-dashes-or-doctype",
-        "(1,20): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "div": true
-          },
-          "comment": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "div",
-                    "children": [
-                      {
-                        "comment": "[CDATA[foo]]"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><div><!--[CDATA[foo]]--></div></body></html>",
-        "noQuirksBodyHtml": "<div><!--[CDATA[foo]]--></div>"
-      }
-    },
-    {
-      "data": "<svg><![CDATA[foo",
-      "errors": [
-        "(1,5): expected-doctype-but-got-start-tag",
-        "(1,17): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "svg svg": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "svg",
-                    "ns": "http://www.w3.org/2000/svg",
-                    "children": [
-                      {
-                        "text": "foo"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><svg>foo</svg></body></html>",
-        "noQuirksBodyHtml": "<svg>foo</svg>"
-      }
-    },
-    {
-      "data": "<svg><![CDATA[foo",
-      "errors": [
-        "(1,5): expected-doctype-but-got-start-tag",
-        "(1,17): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "svg svg": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "svg",
-                    "ns": "http://www.w3.org/2000/svg",
-                    "children": [
-                      {
-                        "text": "foo"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><svg>foo</svg></body></html>",
-        "noQuirksBodyHtml": "<svg>foo</svg>"
-      }
-    },
-    {
-      "data": "<svg><![CDATA[",
-      "errors": [
-        "(1,5): expected-doctype-but-got-start-tag",
-        "(1,14): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "svg svg": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "svg",
-                    "ns": "http://www.w3.org/2000/svg"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><svg></svg></body></html>",
-        "noQuirksBodyHtml": "<svg></svg>"
-      }
-    },
-    {
-      "data": "<svg><![CDATA[]]>",
-      "errors": [
-        "(1,5): expected-doctype-but-got-start-tag",
-        "(1,17): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "svg svg": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "svg",
-                    "ns": "http://www.w3.org/2000/svg"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><svg></svg></body></html>",
-        "noQuirksBodyHtml": "<svg></svg>"
-      }
-    },
-    {
-      "data": "<svg><![CDATA[]] >]]>",
-      "errors": [
-        "(1,5): expected-doctype-but-got-start-tag",
-        "(1,21): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "svg svg": true
-          },
-          "escaped": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "svg",
-                    "ns": "http://www.w3.org/2000/svg",
-                    "children": [
-                      {
-                        "text": "]] >",
-                        "escaped": true
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><svg>]] &gt;</svg></body></html>",
-        "noQuirksBodyHtml": "<svg>]] &gt;</svg>"
-      }
-    },
-    {
-      "data": "<svg><![CDATA[]] >]]>",
-      "errors": [
-        "(1,5): expected-doctype-but-got-start-tag",
-        "(1,21): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "svg svg": true
-          },
-          "escaped": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "svg",
-                    "ns": "http://www.w3.org/2000/svg",
-                    "children": [
-                      {
-                        "text": "]] >",
-                        "escaped": true
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><svg>]] &gt;</svg></body></html>",
-        "noQuirksBodyHtml": "<svg>]] &gt;</svg>"
-      }
-    },
-    {
-      "data": "<svg><![CDATA[]]",
-      "errors": [
-        "(1,5): expected-doctype-but-got-start-tag",
-        "(1,16): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "svg svg": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "svg",
-                    "ns": "http://www.w3.org/2000/svg",
-                    "children": [
-                      {
-                        "text": "]]"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><svg>]]</svg></body></html>",
-        "noQuirksBodyHtml": "<svg>]]</svg>"
-      }
-    },
-    {
-      "data": "<svg><![CDATA[]",
-      "errors": [
-        "(1,5): expected-doctype-but-got-start-tag",
-        "(1,15): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "svg svg": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "svg",
-                    "ns": "http://www.w3.org/2000/svg",
-                    "children": [
-                      {
-                        "text": "]"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><svg>]</svg></body></html>",
-        "noQuirksBodyHtml": "<svg>]</svg>"
-      }
-    },
-    {
-      "data": "<svg><![CDATA[]>a",
-      "errors": [
-        "(1,5): expected-doctype-but-got-start-tag",
-        "(1,17): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "svg svg": true
-          },
-          "escaped": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "svg",
-                    "ns": "http://www.w3.org/2000/svg",
-                    "children": [
-                      {
-                        "text": "]>a",
-                        "escaped": true
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><svg>]&gt;a</svg></body></html>",
-        "noQuirksBodyHtml": "<svg>]&gt;a</svg>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><svg><![CDATA[foo]]]>",
-      "errors": [
-        "(1,36): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "svg svg": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "svg",
-                    "ns": "http://www.w3.org/2000/svg",
-                    "children": [
-                      {
-                        "text": "foo]"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><svg>foo]</svg></body></html>",
-        "noQuirksBodyHtml": "<svg>foo]</svg>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><svg><![CDATA[foo]]]]>",
-      "errors": [
-        "(1,37): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "svg svg": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "svg",
-                    "ns": "http://www.w3.org/2000/svg",
-                    "children": [
-                      {
-                        "text": "foo]]"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><svg>foo]]</svg></body></html>",
-        "noQuirksBodyHtml": "<svg>foo]]</svg>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><svg><![CDATA[foo]]]]]>",
-      "errors": [
-        "(1,38): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "svg svg": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "svg",
-                    "ns": "http://www.w3.org/2000/svg",
-                    "children": [
-                      {
-                        "text": "foo]]]"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><svg>foo]]]</svg></body></html>",
-        "noQuirksBodyHtml": "<svg>foo]]]</svg>"
-      }
-    },
-    {
-      "data": "<svg><foreignObject><div><![CDATA[foo]]>",
-      "errors": [
-        "(1,5): expected-doctype-but-got-start-tag",
-        "(1,27): expected-dashes-or-doctype",
-        "(1,40): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "svg svg": true,
-            "svg foreignObject": true,
-            "div": true
-          },
-          "comment": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "svg",
-                    "ns": "http://www.w3.org/2000/svg",
-                    "children": [
-                      {
-                        "tag": "foreignObject",
-                        "ns": "http://www.w3.org/2000/svg",
-                        "children": [
-                          {
-                            "tag": "div",
-                            "children": [
-                              {
-                                "comment": "[CDATA[foo]]"
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><svg><foreignObject><div><!--[CDATA[foo]]--></div></foreignObject></svg></body></html>",
-        "noQuirksBodyHtml": "<svg><foreignObject><div><!--[CDATA[foo]]--></div></foreignObject></svg>"
-      }
-    },
-    {
-      "data": "<svg><![CDATA[<svg>]]>",
-      "errors": [
-        "(1,5): expected-doctype-but-got-start-tag",
-        "(1,22): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "svg svg": true
-          },
-          "escaped": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "svg",
-                    "ns": "http://www.w3.org/2000/svg",
-                    "children": [
-                      {
-                        "text": "<svg>",
-                        "escaped": true
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><svg>&lt;svg&gt;</svg></body></html>",
-        "noQuirksBodyHtml": "<svg>&lt;svg&gt;</svg>"
-      }
-    },
-    {
-      "data": "<svg><![CDATA[</svg>a]]>",
-      "errors": [
-        "(1,5): expected-doctype-but-got-start-tag",
-        "(1,24): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "svg svg": true
-          },
-          "escaped": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "svg",
-                    "ns": "http://www.w3.org/2000/svg",
-                    "children": [
-                      {
-                        "text": "</svg>a",
-                        "escaped": true
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><svg>&lt;/svg&gt;a</svg></body></html>",
-        "noQuirksBodyHtml": "<svg>&lt;/svg&gt;a</svg>"
-      }
-    },
-    {
-      "data": "<svg><![CDATA[<svg>a",
-      "errors": [
-        "(1,5): expected-doctype-but-got-start-tag",
-        "(1,20): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "svg svg": true
-          },
-          "escaped": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "svg",
-                    "ns": "http://www.w3.org/2000/svg",
-                    "children": [
-                      {
-                        "text": "<svg>a",
-                        "escaped": true
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><svg>&lt;svg&gt;a</svg></body></html>",
-        "noQuirksBodyHtml": "<svg>&lt;svg&gt;a</svg>"
-      }
-    },
-    {
-      "data": "<svg><![CDATA[</svg>a",
-      "errors": [
-        "(1,5): expected-doctype-but-got-start-tag",
-        "(1,21): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "svg svg": true
-          },
-          "escaped": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "svg",
-                    "ns": "http://www.w3.org/2000/svg",
-                    "children": [
-                      {
-                        "text": "</svg>a",
-                        "escaped": true
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><svg>&lt;/svg&gt;a</svg></body></html>",
-        "noQuirksBodyHtml": "<svg>&lt;/svg&gt;a</svg>"
-      }
-    },
-    {
-      "data": "<svg><![CDATA[<svg>]]><path>",
-      "errors": [
-        "(1,5): expected-doctype-but-got-start-tag",
-        "(1,28): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "svg svg": true,
-            "svg path": true
-          },
-          "escaped": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "svg",
-                    "ns": "http://www.w3.org/2000/svg",
-                    "children": [
-                      {
-                        "text": "<svg>",
-                        "escaped": true
-                      },
-                      {
-                        "tag": "path",
-                        "ns": "http://www.w3.org/2000/svg"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><svg>&lt;svg&gt;<path></path></svg></body></html>",
-        "noQuirksBodyHtml": "<svg>&lt;svg&gt;<path></path></svg>"
-      }
-    },
-    {
-      "data": "<svg><![CDATA[<svg>]]></path>",
-      "errors": [
-        "(1,5): expected-doctype-but-got-start-tag",
-        "(1,29): unexpected-end-tag",
-        "(1,29): unexpected-end-tag",
-        "(1,29): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "svg svg": true
-          },
-          "escaped": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "svg",
-                    "ns": "http://www.w3.org/2000/svg",
-                    "children": [
-                      {
-                        "text": "<svg>",
-                        "escaped": true
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><svg>&lt;svg&gt;</svg></body></html>",
-        "noQuirksBodyHtml": "<svg>&lt;svg&gt;</svg>"
-      }
-    },
-    {
-      "data": "<svg><![CDATA[<svg>]]><!--path-->",
-      "errors": [
-        "(1,5): expected-doctype-but-got-start-tag",
-        "(1,33): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "svg svg": true
-          },
-          "escaped": true,
-          "comment": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "svg",
-                    "ns": "http://www.w3.org/2000/svg",
-                    "children": [
-                      {
-                        "text": "<svg>",
-                        "escaped": true
-                      },
-                      {
-                        "comment": "path"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><svg>&lt;svg&gt;<!--path--></svg></body></html>",
-        "noQuirksBodyHtml": "<svg>&lt;svg&gt;<!--path--></svg>"
-      }
-    },
-    {
-      "data": "<svg><![CDATA[<svg>]]>path",
-      "errors": [
-        "(1,5): expected-doctype-but-got-start-tag",
-        "(1,26): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "svg svg": true
-          },
-          "escaped": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "svg",
-                    "ns": "http://www.w3.org/2000/svg",
-                    "children": [
-                      {
-                        "text": "<svg>path",
-                        "escaped": true
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><svg>&lt;svg&gt;path</svg></body></html>",
-        "noQuirksBodyHtml": "<svg>&lt;svg&gt;path</svg>"
-      }
-    },
-    {
-      "data": "<svg><![CDATA[<!--svg-->]]>",
-      "errors": [
-        "(1,5): expected-doctype-but-got-start-tag",
-        "(1,27): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "svg svg": true
-          },
-          "escaped": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "svg",
-                    "ns": "http://www.w3.org/2000/svg",
-                    "children": [
-                      {
-                        "text": "<!--svg-->",
-                        "escaped": true
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><svg>&lt;!--svg--&gt;</svg></body></html>",
-        "noQuirksBodyHtml": "<svg>&lt;!--svg--&gt;</svg>"
-      }
-    }
-  ],
-  "tests22.dat": [
-    {
-      "data": "<a><b><big><em><strong><div>X</a>",
-      "errors": [
-        "(1,3): expected-doctype-but-got-start-tag",
-        "(1,33): adoption-agency-1.3",
-        "(1,33): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "a": true,
-            "b": true,
-            "big": true,
-            "em": true,
-            "strong": true,
-            "div": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "a",
-                    "children": [
-                      {
-                        "tag": "b",
-                        "children": [
-                          {
-                            "tag": "big",
-                            "children": [
-                              {
-                                "tag": "em",
-                                "children": [
-                                  {
-                                    "tag": "strong"
-                                  }
-                                ]
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "big",
-                    "children": [
-                      {
-                        "tag": "em",
-                        "children": [
-                          {
-                            "tag": "strong",
-                            "children": [
-                              {
-                                "tag": "div",
-                                "children": [
-                                  {
-                                    "tag": "a",
-                                    "children": [
-                                      {
-                                        "text": "X"
-                                      }
-                                    ]
-                                  }
-                                ]
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><a><b><big><em><strong></strong></em></big></b></a><big><em><strong><div><a>X</a></div></strong></em></big></body></html>",
-        "noQuirksBodyHtml": "<a><b><big><em><strong></strong></em></big></b></a><big><em><strong><div><a>X</a></div></strong></em></big>"
-      }
-    },
-    {
-      "data": "<a><b><div id=1><div id=2><div id=3><div id=4><div id=5><div id=6><div id=7><div id=8>A</a>",
-      "errors": [
-        "(1,3): expected-doctype-but-got-start-tag",
-        "(1,91): adoption-agency-1.3",
-        "(1,91): adoption-agency-1.3",
-        "(1,91): adoption-agency-1.3",
-        "(1,91): adoption-agency-1.3",
-        "(1,91): adoption-agency-1.3",
-        "(1,91): adoption-agency-1.3",
-        "(1,91): adoption-agency-1.3",
-        "(1,91): adoption-agency-1.3",
-        "(1,91): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "a": true,
-            "b": true,
-            "div": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "a",
-                    "children": [
-                      {
-                        "tag": "b"
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "b",
-                    "children": [
-                      {
-                        "tag": "div",
-                        "attrs": [
-                          {
-                            "name": "id",
-                            "value": "1"
-                          }
-                        ],
-                        "children": [
-                          {
-                            "tag": "a"
-                          },
-                          {
-                            "tag": "div",
-                            "attrs": [
-                              {
-                                "name": "id",
-                                "value": "2"
-                              }
-                            ],
-                            "children": [
-                              {
-                                "tag": "a"
-                              },
-                              {
-                                "tag": "div",
-                                "attrs": [
-                                  {
-                                    "name": "id",
-                                    "value": "3"
-                                  }
-                                ],
-                                "children": [
-                                  {
-                                    "tag": "a"
-                                  },
-                                  {
-                                    "tag": "div",
-                                    "attrs": [
-                                      {
-                                        "name": "id",
-                                        "value": "4"
-                                      }
-                                    ],
-                                    "children": [
-                                      {
-                                        "tag": "a"
-                                      },
-                                      {
-                                        "tag": "div",
-                                        "attrs": [
-                                          {
-                                            "name": "id",
-                                            "value": "5"
-                                          }
-                                        ],
-                                        "children": [
-                                          {
-                                            "tag": "a"
-                                          },
-                                          {
-                                            "tag": "div",
-                                            "attrs": [
-                                              {
-                                                "name": "id",
-                                                "value": "6"
-                                              }
-                                            ],
-                                            "children": [
-                                              {
-                                                "tag": "a"
-                                              },
-                                              {
-                                                "tag": "div",
-                                                "attrs": [
-                                                  {
-                                                    "name": "id",
-                                                    "value": "7"
-                                                  }
-                                                ],
-                                                "children": [
-                                                  {
-                                                    "tag": "a"
-                                                  },
-                                                  {
-                                                    "tag": "div",
-                                                    "attrs": [
-                                                      {
-                                                        "name": "id",
-                                                        "value": "8"
-                                                      }
-                                                    ],
-                                                    "children": [
-                                                      {
-                                                        "tag": "a",
-                                                        "children": [
-                                                          {
-                                                            "text": "A"
-                                                          }
-                                                        ]
-                                                      }
-                                                    ]
-                                                  }
-                                                ]
-                                              }
-                                            ]
-                                          }
-                                        ]
-                                      }
-                                    ]
-                                  }
-                                ]
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><a><b></b></a><b><div id=\"1\"><a></a><div id=\"2\"><a></a><div id=\"3\"><a></a><div id=\"4\"><a></a><div id=\"5\"><a></a><div id=\"6\"><a></a><div id=\"7\"><a></a><div id=\"8\"><a>A</a></div></div></div></div></div></div></div></div></b></body></html>",
-        "noQuirksBodyHtml": "<a><b></b></a><b><div id=\"1\"><a></a><div id=\"2\"><a></a><div id=\"3\"><a></a><div id=\"4\"><a></a><div id=\"5\"><a></a><div id=\"6\"><a></a><div id=\"7\"><a></a><div id=\"8\"><a>A</a></div></div></div></div></div></div></div></div></b>"
-      }
-    },
-    {
-      "data": "<a><b><div id=1><div id=2><div id=3><div id=4><div id=5><div id=6><div id=7><div id=8><div id=9>A</a>",
-      "errors": [
-        "(1,3): expected-doctype-but-got-start-tag",
-        "(1,101): adoption-agency-1.3",
-        "(1,101): adoption-agency-1.3",
-        "(1,101): adoption-agency-1.3",
-        "(1,101): adoption-agency-1.3",
-        "(1,101): adoption-agency-1.3",
-        "(1,101): adoption-agency-1.3",
-        "(1,101): adoption-agency-1.3",
-        "(1,101): adoption-agency-1.3",
-        "(1,101): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "a": true,
-            "b": true,
-            "div": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "a",
-                    "children": [
-                      {
-                        "tag": "b"
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "b",
-                    "children": [
-                      {
-                        "tag": "div",
-                        "attrs": [
-                          {
-                            "name": "id",
-                            "value": "1"
-                          }
-                        ],
-                        "children": [
-                          {
-                            "tag": "a"
-                          },
-                          {
-                            "tag": "div",
-                            "attrs": [
-                              {
-                                "name": "id",
-                                "value": "2"
-                              }
-                            ],
-                            "children": [
-                              {
-                                "tag": "a"
-                              },
-                              {
-                                "tag": "div",
-                                "attrs": [
-                                  {
-                                    "name": "id",
-                                    "value": "3"
-                                  }
-                                ],
-                                "children": [
-                                  {
-                                    "tag": "a"
-                                  },
-                                  {
-                                    "tag": "div",
-                                    "attrs": [
-                                      {
-                                        "name": "id",
-                                        "value": "4"
-                                      }
-                                    ],
-                                    "children": [
-                                      {
-                                        "tag": "a"
-                                      },
-                                      {
-                                        "tag": "div",
-                                        "attrs": [
-                                          {
-                                            "name": "id",
-                                            "value": "5"
-                                          }
-                                        ],
-                                        "children": [
-                                          {
-                                            "tag": "a"
-                                          },
-                                          {
-                                            "tag": "div",
-                                            "attrs": [
-                                              {
-                                                "name": "id",
-                                                "value": "6"
-                                              }
-                                            ],
-                                            "children": [
-                                              {
-                                                "tag": "a"
-                                              },
-                                              {
-                                                "tag": "div",
-                                                "attrs": [
-                                                  {
-                                                    "name": "id",
-                                                    "value": "7"
-                                                  }
-                                                ],
-                                                "children": [
-                                                  {
-                                                    "tag": "a"
-                                                  },
-                                                  {
-                                                    "tag": "div",
-                                                    "attrs": [
-                                                      {
-                                                        "name": "id",
-                                                        "value": "8"
-                                                      }
-                                                    ],
-                                                    "children": [
-                                                      {
-                                                        "tag": "a",
-                                                        "children": [
-                                                          {
-                                                            "tag": "div",
-                                                            "attrs": [
-                                                              {
-                                                                "name": "id",
-                                                                "value": "9"
-                                                              }
-                                                            ],
-                                                            "children": [
-                                                              {
-                                                                "text": "A"
-                                                              }
-                                                            ]
-                                                          }
-                                                        ]
-                                                      }
-                                                    ]
-                                                  }
-                                                ]
-                                              }
-                                            ]
-                                          }
-                                        ]
-                                      }
-                                    ]
-                                  }
-                                ]
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><a><b></b></a><b><div id=\"1\"><a></a><div id=\"2\"><a></a><div id=\"3\"><a></a><div id=\"4\"><a></a><div id=\"5\"><a></a><div id=\"6\"><a></a><div id=\"7\"><a></a><div id=\"8\"><a><div id=\"9\">A</div></a></div></div></div></div></div></div></div></div></b></body></html>",
-        "noQuirksBodyHtml": "<a><b></b></a><b><div id=\"1\"><a></a><div id=\"2\"><a></a><div id=\"3\"><a></a><div id=\"4\"><a></a><div id=\"5\"><a></a><div id=\"6\"><a></a><div id=\"7\"><a></a><div id=\"8\"><a><div id=\"9\">A</div></a></div></div></div></div></div></div></div></div></b>"
-      }
-    },
-    {
-      "data": "<a><b><div id=1><div id=2><div id=3><div id=4><div id=5><div id=6><div id=7><div id=8><div id=9><div id=10>A</a>",
-      "errors": [
-        "(1,3): expected-doctype-but-got-start-tag",
-        "(1,112): adoption-agency-1.3",
-        "(1,112): adoption-agency-1.3",
-        "(1,112): adoption-agency-1.3",
-        "(1,112): adoption-agency-1.3",
-        "(1,112): adoption-agency-1.3",
-        "(1,112): adoption-agency-1.3",
-        "(1,112): adoption-agency-1.3",
-        "(1,112): adoption-agency-1.3",
-        "(1,112): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "a": true,
-            "b": true,
-            "div": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "a",
-                    "children": [
-                      {
-                        "tag": "b"
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "b",
-                    "children": [
-                      {
-                        "tag": "div",
-                        "attrs": [
-                          {
-                            "name": "id",
-                            "value": "1"
-                          }
-                        ],
-                        "children": [
-                          {
-                            "tag": "a"
-                          },
-                          {
-                            "tag": "div",
-                            "attrs": [
-                              {
-                                "name": "id",
-                                "value": "2"
-                              }
-                            ],
-                            "children": [
-                              {
-                                "tag": "a"
-                              },
-                              {
-                                "tag": "div",
-                                "attrs": [
-                                  {
-                                    "name": "id",
-                                    "value": "3"
-                                  }
-                                ],
-                                "children": [
-                                  {
-                                    "tag": "a"
-                                  },
-                                  {
-                                    "tag": "div",
-                                    "attrs": [
-                                      {
-                                        "name": "id",
-                                        "value": "4"
-                                      }
-                                    ],
-                                    "children": [
-                                      {
-                                        "tag": "a"
-                                      },
-                                      {
-                                        "tag": "div",
-                                        "attrs": [
-                                          {
-                                            "name": "id",
-                                            "value": "5"
-                                          }
-                                        ],
-                                        "children": [
-                                          {
-                                            "tag": "a"
-                                          },
-                                          {
-                                            "tag": "div",
-                                            "attrs": [
-                                              {
-                                                "name": "id",
-                                                "value": "6"
-                                              }
-                                            ],
-                                            "children": [
-                                              {
-                                                "tag": "a"
-                                              },
-                                              {
-                                                "tag": "div",
-                                                "attrs": [
-                                                  {
-                                                    "name": "id",
-                                                    "value": "7"
-                                                  }
-                                                ],
-                                                "children": [
-                                                  {
-                                                    "tag": "a"
-                                                  },
-                                                  {
-                                                    "tag": "div",
-                                                    "attrs": [
-                                                      {
-                                                        "name": "id",
-                                                        "value": "8"
-                                                      }
-                                                    ],
-                                                    "children": [
-                                                      {
-                                                        "tag": "a",
-                                                        "children": [
-                                                          {
-                                                            "tag": "div",
-                                                            "attrs": [
-                                                              {
-                                                                "name": "id",
-                                                                "value": "9"
-                                                              }
-                                                            ],
-                                                            "children": [
-                                                              {
-                                                                "tag": "div",
-                                                                "attrs": [
-                                                                  {
-                                                                    "name": "id",
-                                                                    "value": "10"
-                                                                  }
-                                                                ],
-                                                                "children": [
-                                                                  {
-                                                                    "text": "A"
-                                                                  }
-                                                                ]
-                                                              }
-                                                            ]
-                                                          }
-                                                        ]
-                                                      }
-                                                    ]
-                                                  }
-                                                ]
-                                              }
-                                            ]
-                                          }
-                                        ]
-                                      }
-                                    ]
-                                  }
-                                ]
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><a><b></b></a><b><div id=\"1\"><a></a><div id=\"2\"><a></a><div id=\"3\"><a></a><div id=\"4\"><a></a><div id=\"5\"><a></a><div id=\"6\"><a></a><div id=\"7\"><a></a><div id=\"8\"><a><div id=\"9\"><div id=\"10\">A</div></div></a></div></div></div></div></div></div></div></div></b></body></html>",
-        "noQuirksBodyHtml": "<a><b></b></a><b><div id=\"1\"><a></a><div id=\"2\"><a></a><div id=\"3\"><a></a><div id=\"4\"><a></a><div id=\"5\"><a></a><div id=\"6\"><a></a><div id=\"7\"><a></a><div id=\"8\"><a><div id=\"9\"><div id=\"10\">A</div></div></a></div></div></div></div></div></div></div></div></b>"
-      }
-    },
-    {
-      "data": "<cite><b><cite><i><cite><i><cite><i><div>X</b>TEST",
-      "errors": [
-        "(1,6): expected-doctype-but-got-start-tag",
-        "(1,46): adoption-agency-1.3",
-        "(1,50): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "cite": true,
-            "b": true,
-            "i": true,
-            "div": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "cite",
-                    "children": [
-                      {
-                        "tag": "b",
-                        "children": [
-                          {
-                            "tag": "cite",
-                            "children": [
-                              {
-                                "tag": "i",
-                                "children": [
-                                  {
-                                    "tag": "cite",
-                                    "children": [
-                                      {
-                                        "tag": "i",
-                                        "children": [
-                                          {
-                                            "tag": "cite",
-                                            "children": [
-                                              {
-                                                "tag": "i"
-                                              }
-                                            ]
-                                          }
-                                        ]
-                                      }
-                                    ]
-                                  }
-                                ]
-                              }
-                            ]
-                          }
-                        ]
-                      },
-                      {
-                        "tag": "i",
-                        "children": [
-                          {
-                            "tag": "i",
-                            "children": [
-                              {
-                                "tag": "div",
-                                "children": [
-                                  {
-                                    "tag": "b",
-                                    "children": [
-                                      {
-                                        "text": "X"
-                                      }
-                                    ]
-                                  },
-                                  {
-                                    "text": "TEST"
-                                  }
-                                ]
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><cite><b><cite><i><cite><i><cite><i></i></cite></i></cite></i></cite></b><i><i><div><b>X</b>TEST</div></i></i></cite></body></html>",
-        "noQuirksBodyHtml": "<cite><b><cite><i><cite><i><cite><i></i></cite></i></cite></i></cite></b><i><i><div><b>X</b>TEST</div></i></i></cite>"
-      }
-    }
-  ],
-  "tests23.dat": [
-    {
-      "data": "<p><font size=4><font color=red><font size=4><font size=4><font size=4><font size=4><font size=4><font color=red><p>X",
-      "errors": [
-        "(1,3): expected-doctype-but-got-start-tag",
-        "(1,116): unexpected-end-tag",
-        "(1,117): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "p": true,
-            "font": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "p",
-                    "children": [
-                      {
-                        "tag": "font",
-                        "attrs": [
-                          {
-                            "name": "size",
-                            "value": "4"
-                          }
-                        ],
-                        "children": [
-                          {
-                            "tag": "font",
-                            "attrs": [
-                              {
-                                "name": "color",
-                                "value": "red"
-                              }
-                            ],
-                            "children": [
-                              {
-                                "tag": "font",
-                                "attrs": [
-                                  {
-                                    "name": "size",
-                                    "value": "4"
-                                  }
-                                ],
-                                "children": [
-                                  {
-                                    "tag": "font",
-                                    "attrs": [
-                                      {
-                                        "name": "size",
-                                        "value": "4"
-                                      }
-                                    ],
-                                    "children": [
-                                      {
-                                        "tag": "font",
-                                        "attrs": [
-                                          {
-                                            "name": "size",
-                                            "value": "4"
-                                          }
-                                        ],
-                                        "children": [
-                                          {
-                                            "tag": "font",
-                                            "attrs": [
-                                              {
-                                                "name": "size",
-                                                "value": "4"
-                                              }
-                                            ],
-                                            "children": [
-                                              {
-                                                "tag": "font",
-                                                "attrs": [
-                                                  {
-                                                    "name": "size",
-                                                    "value": "4"
-                                                  }
-                                                ],
-                                                "children": [
-                                                  {
-                                                    "tag": "font",
-                                                    "attrs": [
-                                                      {
-                                                        "name": "color",
-                                                        "value": "red"
-                                                      }
-                                                    ]
-                                                  }
-                                                ]
-                                              }
-                                            ]
-                                          }
-                                        ]
-                                      }
-                                    ]
-                                  }
-                                ]
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "p",
-                    "children": [
-                      {
-                        "tag": "font",
-                        "attrs": [
-                          {
-                            "name": "color",
-                            "value": "red"
-                          }
-                        ],
-                        "children": [
-                          {
-                            "tag": "font",
-                            "attrs": [
-                              {
-                                "name": "size",
-                                "value": "4"
-                              }
-                            ],
-                            "children": [
-                              {
-                                "tag": "font",
-                                "attrs": [
-                                  {
-                                    "name": "size",
-                                    "value": "4"
-                                  }
-                                ],
-                                "children": [
-                                  {
-                                    "tag": "font",
-                                    "attrs": [
-                                      {
-                                        "name": "size",
-                                        "value": "4"
-                                      }
-                                    ],
-                                    "children": [
-                                      {
-                                        "tag": "font",
-                                        "attrs": [
-                                          {
-                                            "name": "color",
-                                            "value": "red"
-                                          }
-                                        ],
-                                        "children": [
-                                          {
-                                            "text": "X"
-                                          }
-                                        ]
-                                      }
-                                    ]
-                                  }
-                                ]
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><p><font size=\"4\"><font color=\"red\"><font size=\"4\"><font size=\"4\"><font size=\"4\"><font size=\"4\"><font size=\"4\"><font color=\"red\"></font></font></font></font></font></font></font></font></p><p><font color=\"red\"><font size=\"4\"><font size=\"4\"><font size=\"4\"><font color=\"red\">X</font></font></font></font></font></p></body></html>",
-        "noQuirksBodyHtml": "<p><font size=\"4\"><font color=\"red\"><font size=\"4\"><font size=\"4\"><font size=\"4\"><font size=\"4\"><font size=\"4\"><font color=\"red\"></font></font></font></font></font></font></font></font></p><p><font color=\"red\"><font size=\"4\"><font size=\"4\"><font size=\"4\"><font color=\"red\">X</font></font></font></font></font></p>"
-      }
-    },
-    {
-      "data": "<p><font size=4><font size=4><font size=4><font size=4><p>X",
-      "errors": [
-        "(1,3): expected-doctype-but-got-start-tag",
-        "(1,58): unexpected-end-tag",
-        "(1,59): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "p": true,
-            "font": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "p",
-                    "children": [
-                      {
-                        "tag": "font",
-                        "attrs": [
-                          {
-                            "name": "size",
-                            "value": "4"
-                          }
-                        ],
-                        "children": [
-                          {
-                            "tag": "font",
-                            "attrs": [
-                              {
-                                "name": "size",
-                                "value": "4"
-                              }
-                            ],
-                            "children": [
-                              {
-                                "tag": "font",
-                                "attrs": [
-                                  {
-                                    "name": "size",
-                                    "value": "4"
-                                  }
-                                ],
-                                "children": [
-                                  {
-                                    "tag": "font",
-                                    "attrs": [
-                                      {
-                                        "name": "size",
-                                        "value": "4"
-                                      }
-                                    ]
-                                  }
-                                ]
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "p",
-                    "children": [
-                      {
-                        "tag": "font",
-                        "attrs": [
-                          {
-                            "name": "size",
-                            "value": "4"
-                          }
-                        ],
-                        "children": [
-                          {
-                            "tag": "font",
-                            "attrs": [
-                              {
-                                "name": "size",
-                                "value": "4"
-                              }
-                            ],
-                            "children": [
-                              {
-                                "tag": "font",
-                                "attrs": [
-                                  {
-                                    "name": "size",
-                                    "value": "4"
-                                  }
-                                ],
-                                "children": [
-                                  {
-                                    "text": "X"
-                                  }
-                                ]
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><p><font size=\"4\"><font size=\"4\"><font size=\"4\"><font size=\"4\"></font></font></font></font></p><p><font size=\"4\"><font size=\"4\"><font size=\"4\">X</font></font></font></p></body></html>",
-        "noQuirksBodyHtml": "<p><font size=\"4\"><font size=\"4\"><font size=\"4\"><font size=\"4\"></font></font></font></font></p><p><font size=\"4\"><font size=\"4\"><font size=\"4\">X</font></font></font></p>"
-      }
-    },
-    {
-      "data": "<p><font size=4><font size=4><font size=4><font size=\"5\"><font size=4><p>X",
-      "errors": [
-        "(1,3): expected-doctype-but-got-start-tag",
-        "(1,73): unexpected-end-tag",
-        "(1,74): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "p": true,
-            "font": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "p",
-                    "children": [
-                      {
-                        "tag": "font",
-                        "attrs": [
-                          {
-                            "name": "size",
-                            "value": "4"
-                          }
-                        ],
-                        "children": [
-                          {
-                            "tag": "font",
-                            "attrs": [
-                              {
-                                "name": "size",
-                                "value": "4"
-                              }
-                            ],
-                            "children": [
-                              {
-                                "tag": "font",
-                                "attrs": [
-                                  {
-                                    "name": "size",
-                                    "value": "4"
-                                  }
-                                ],
-                                "children": [
-                                  {
-                                    "tag": "font",
-                                    "attrs": [
-                                      {
-                                        "name": "size",
-                                        "value": "5"
-                                      }
-                                    ],
-                                    "children": [
-                                      {
-                                        "tag": "font",
-                                        "attrs": [
-                                          {
-                                            "name": "size",
-                                            "value": "4"
-                                          }
-                                        ]
-                                      }
-                                    ]
-                                  }
-                                ]
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "p",
-                    "children": [
-                      {
-                        "tag": "font",
-                        "attrs": [
-                          {
-                            "name": "size",
-                            "value": "4"
-                          }
-                        ],
-                        "children": [
-                          {
-                            "tag": "font",
-                            "attrs": [
-                              {
-                                "name": "size",
-                                "value": "4"
-                              }
-                            ],
-                            "children": [
-                              {
-                                "tag": "font",
-                                "attrs": [
-                                  {
-                                    "name": "size",
-                                    "value": "5"
-                                  }
-                                ],
-                                "children": [
-                                  {
-                                    "tag": "font",
-                                    "attrs": [
-                                      {
-                                        "name": "size",
-                                        "value": "4"
-                                      }
-                                    ],
-                                    "children": [
-                                      {
-                                        "text": "X"
-                                      }
-                                    ]
-                                  }
-                                ]
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><p><font size=\"4\"><font size=\"4\"><font size=\"4\"><font size=\"5\"><font size=\"4\"></font></font></font></font></font></p><p><font size=\"4\"><font size=\"4\"><font size=\"5\"><font size=\"4\">X</font></font></font></font></p></body></html>",
-        "noQuirksBodyHtml": "<p><font size=\"4\"><font size=\"4\"><font size=\"4\"><font size=\"5\"><font size=\"4\"></font></font></font></font></font></p><p><font size=\"4\"><font size=\"4\"><font size=\"5\"><font size=\"4\">X</font></font></font></font></p>"
-      }
-    },
-    {
-      "data": "<p><font size=4 id=a><font size=4 id=b><font size=4><font size=4><p>X",
-      "errors": [
-        "(1,3): expected-doctype-but-got-start-tag",
-        "(1,68): unexpected-end-tag",
-        "(1,69): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "p": true,
-            "font": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "p",
-                    "children": [
-                      {
-                        "tag": "font",
-                        "attrs": [
-                          {
-                            "name": "id",
-                            "value": "a"
-                          },
-                          {
-                            "name": "size",
-                            "value": "4"
-                          }
-                        ],
-                        "children": [
-                          {
-                            "tag": "font",
-                            "attrs": [
-                              {
-                                "name": "id",
-                                "value": "b"
-                              },
-                              {
-                                "name": "size",
-                                "value": "4"
-                              }
-                            ],
-                            "children": [
-                              {
-                                "tag": "font",
-                                "attrs": [
-                                  {
-                                    "name": "size",
-                                    "value": "4"
-                                  }
-                                ],
-                                "children": [
-                                  {
-                                    "tag": "font",
-                                    "attrs": [
-                                      {
-                                        "name": "size",
-                                        "value": "4"
-                                      }
-                                    ]
-                                  }
-                                ]
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "p",
-                    "children": [
-                      {
-                        "tag": "font",
-                        "attrs": [
-                          {
-                            "name": "id",
-                            "value": "a"
-                          },
-                          {
-                            "name": "size",
-                            "value": "4"
-                          }
-                        ],
-                        "children": [
-                          {
-                            "tag": "font",
-                            "attrs": [
-                              {
-                                "name": "id",
-                                "value": "b"
-                              },
-                              {
-                                "name": "size",
-                                "value": "4"
-                              }
-                            ],
-                            "children": [
-                              {
-                                "tag": "font",
-                                "attrs": [
-                                  {
-                                    "name": "size",
-                                    "value": "4"
-                                  }
-                                ],
-                                "children": [
-                                  {
-                                    "tag": "font",
-                                    "attrs": [
-                                      {
-                                        "name": "size",
-                                        "value": "4"
-                                      }
-                                    ],
-                                    "children": [
-                                      {
-                                        "text": "X"
-                                      }
-                                    ]
-                                  }
-                                ]
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><p><font size=\"4\" id=\"a\"><font size=\"4\" id=\"b\"><font size=\"4\"><font size=\"4\"></font></font></font></font></p><p><font size=\"4\" id=\"a\"><font size=\"4\" id=\"b\"><font size=\"4\"><font size=\"4\">X</font></font></font></font></p></body></html>",
-        "noQuirksBodyHtml": "<p><font size=\"4\" id=\"a\"><font size=\"4\" id=\"b\"><font size=\"4\"><font size=\"4\"></font></font></font></font></p><p><font size=\"4\" id=\"a\"><font size=\"4\" id=\"b\"><font size=\"4\"><font size=\"4\">X</font></font></font></font></p>"
-      }
-    },
-    {
-      "data": "<p><b id=a><b id=a><b id=a><b><object><b id=a><b id=a>X</object><p>Y",
-      "errors": [
-        "(1,3): expected-doctype-but-got-start-tag",
-        "(1,64): end-tag-too-early",
-        "(1,67): unexpected-end-tag",
-        "(1,68): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "p": true,
-            "b": true,
-            "object": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "p",
-                    "children": [
-                      {
-                        "tag": "b",
-                        "attrs": [
-                          {
-                            "name": "id",
-                            "value": "a"
-                          }
-                        ],
-                        "children": [
-                          {
-                            "tag": "b",
-                            "attrs": [
-                              {
-                                "name": "id",
-                                "value": "a"
-                              }
-                            ],
-                            "children": [
-                              {
-                                "tag": "b",
-                                "attrs": [
-                                  {
-                                    "name": "id",
-                                    "value": "a"
-                                  }
-                                ],
-                                "children": [
-                                  {
-                                    "tag": "b",
-                                    "children": [
-                                      {
-                                        "tag": "object",
-                                        "children": [
-                                          {
-                                            "tag": "b",
-                                            "attrs": [
-                                              {
-                                                "name": "id",
-                                                "value": "a"
-                                              }
-                                            ],
-                                            "children": [
-                                              {
-                                                "tag": "b",
-                                                "attrs": [
-                                                  {
-                                                    "name": "id",
-                                                    "value": "a"
-                                                  }
-                                                ],
-                                                "children": [
-                                                  {
-                                                    "text": "X"
-                                                  }
-                                                ]
-                                              }
-                                            ]
-                                          }
-                                        ]
-                                      }
-                                    ]
-                                  }
-                                ]
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "p",
-                    "children": [
-                      {
-                        "tag": "b",
-                        "attrs": [
-                          {
-                            "name": "id",
-                            "value": "a"
-                          }
-                        ],
-                        "children": [
-                          {
-                            "tag": "b",
-                            "attrs": [
-                              {
-                                "name": "id",
-                                "value": "a"
-                              }
-                            ],
-                            "children": [
-                              {
-                                "tag": "b",
-                                "attrs": [
-                                  {
-                                    "name": "id",
-                                    "value": "a"
-                                  }
-                                ],
-                                "children": [
-                                  {
-                                    "tag": "b",
-                                    "children": [
-                                      {
-                                        "text": "Y"
-                                      }
-                                    ]
-                                  }
-                                ]
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><p><b id=\"a\"><b id=\"a\"><b id=\"a\"><b><object><b id=\"a\"><b id=\"a\">X</b></b></object></b></b></b></b></p><p><b id=\"a\"><b id=\"a\"><b id=\"a\"><b>Y</b></b></b></b></p></body></html>",
-        "noQuirksBodyHtml": "<p><b id=\"a\"><b id=\"a\"><b id=\"a\"><b><object><b id=\"a\"><b id=\"a\">X</b></b></object></b></b></b></b></p><p><b id=\"a\"><b id=\"a\"><b id=\"a\"><b>Y</b></b></b></b></p>"
-      }
-    }
-  ],
-  "tests24.dat": [
-    {
-      "data": "<!DOCTYPE html>&NotEqualTilde;",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "≂̸"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body>≂̸</body></html>",
-        "noQuirksBodyHtml": "≂̸"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html>&NotEqualTilde;A",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "≂̸A"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body>≂̸A</body></html>",
-        "noQuirksBodyHtml": "≂̸A"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html>&ThickSpace;",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "  "
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body>  </body></html>",
-        "noQuirksBodyHtml": "  "
-      }
-    },
-    {
-      "data": "<!DOCTYPE html>&ThickSpace;A",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "  A"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body>  A</body></html>",
-        "noQuirksBodyHtml": "  A"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html>&NotSubset;",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "⊂⃒"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body>⊂⃒</body></html>",
-        "noQuirksBodyHtml": "⊂⃒"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html>&NotSubset;A",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "⊂⃒A"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body>⊂⃒A</body></html>",
-        "noQuirksBodyHtml": "⊂⃒A"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html>&Gopf;",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "𝔾"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body>𝔾</body></html>",
-        "noQuirksBodyHtml": "𝔾"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html>&Gopf;A",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "𝔾A"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body>𝔾A</body></html>",
-        "noQuirksBodyHtml": "𝔾A"
-      }
-    }
-  ],
-  "tests25.dat": [
-    {
-      "data": "<!DOCTYPE html><body><foo>A",
-      "errors": [
-        "(1,27): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "foo": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "foo",
-                    "children": [
-                      {
-                        "text": "A"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><foo>A</foo></body></html>",
-        "noQuirksBodyHtml": "<foo>A</foo>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><body><area>A",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "area": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "area"
-                  },
-                  {
-                    "text": "A"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><area>A</body></html>",
-        "noQuirksBodyHtml": "<area>A"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><body><base>A",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "base": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "base"
-                  },
-                  {
-                    "text": "A"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><base>A</body></html>",
-        "noQuirksBodyHtml": "<base>A"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><body><basefont>A",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "basefont": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "basefont"
-                  },
-                  {
-                    "text": "A"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><basefont>A</body></html>",
-        "noQuirksBodyHtml": "<basefont>A"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><body><bgsound>A",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "bgsound": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "bgsound"
-                  },
-                  {
-                    "text": "A"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><bgsound>A</body></html>",
-        "noQuirksBodyHtml": "<bgsound>A"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><body><br>A",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "br": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "br"
-                  },
-                  {
-                    "text": "A"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><br>A</body></html>",
-        "noQuirksBodyHtml": "<br>A"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><body><col>A",
-      "errors": [
-        "(1,26): unexpected-start-tag-ignored"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "A"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body>A</body></html>",
-        "noQuirksBodyHtml": "A"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><body><command>A",
-      "errors": [
-        "eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "command": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "command",
-                    "children": [
-                      {
-                        "text": "A"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><command>A</command></body></html>",
-        "noQuirksBodyHtml": "<command>A</command>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><body><embed>A",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "embed": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "embed"
-                  },
-                  {
-                    "text": "A"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><embed>A</body></html>",
-        "noQuirksBodyHtml": "<embed>A"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><body><frame>A",
-      "errors": [
-        "(1,28): unexpected-start-tag-ignored"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "A"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body>A</body></html>",
-        "noQuirksBodyHtml": "A"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><body><hr>A",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "hr": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "hr"
-                  },
-                  {
-                    "text": "A"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><hr>A</body></html>",
-        "noQuirksBodyHtml": "<hr>A"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><body><img>A",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "img": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "img"
-                  },
-                  {
-                    "text": "A"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><img>A</body></html>",
-        "noQuirksBodyHtml": "<img>A"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><body><input>A",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "input": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "input"
-                  },
-                  {
-                    "text": "A"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><input>A</body></html>",
-        "noQuirksBodyHtml": "<input>A"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><body><keygen>A",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "keygen": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "keygen"
-                  },
-                  {
-                    "text": "A"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><keygen>A</body></html>",
-        "noQuirksBodyHtml": "<keygen>A"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><body><link>A",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "link": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "link"
-                  },
-                  {
-                    "text": "A"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><link>A</body></html>",
-        "noQuirksBodyHtml": "<link>A"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><body><meta>A",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "meta": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "meta"
-                  },
-                  {
-                    "text": "A"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><meta>A</body></html>",
-        "noQuirksBodyHtml": "<meta>A"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><body><param>A",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "param": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "param"
-                  },
-                  {
-                    "text": "A"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><param>A</body></html>",
-        "noQuirksBodyHtml": "<param>A"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><body><source>A",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "source": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "source"
-                  },
-                  {
-                    "text": "A"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><source>A</body></html>",
-        "noQuirksBodyHtml": "<source>A"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><body><track>A",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "track": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "track"
-                  },
-                  {
-                    "text": "A"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><track>A</body></html>",
-        "noQuirksBodyHtml": "<track>A"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><body><wbr>A",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "wbr": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "wbr"
-                  },
-                  {
-                    "text": "A"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><wbr>A</body></html>",
-        "noQuirksBodyHtml": "<wbr>A"
-      }
-    }
-  ],
-  "tests26.dat": [
-    {
-      "data": "<!DOCTYPE html><body><a href='#1'><nobr>1<nobr></a><br><a href='#2'><nobr>2<nobr></a><br><a href='#3'><nobr>3<nobr></a>",
-      "errors": [
-        "(1,47): unexpected-start-tag-implies-end-tag",
-        "(1,51): adoption-agency-1.3",
-        "(1,74): unexpected-start-tag-implies-end-tag",
-        "(1,74): adoption-agency-1.3",
-        "(1,81): unexpected-start-tag-implies-end-tag",
-        "(1,85): adoption-agency-1.3",
-        "(1,108): unexpected-start-tag-implies-end-tag",
-        "(1,108): adoption-agency-1.3",
-        "(1,115): unexpected-start-tag-implies-end-tag",
-        "(1,119): adoption-agency-1.3"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "a": true,
-            "nobr": true,
-            "br": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "a",
-                    "attrs": [
-                      {
-                        "name": "href",
-                        "value": "#1"
-                      }
-                    ],
-                    "children": [
-                      {
-                        "tag": "nobr",
-                        "children": [
-                          {
-                            "text": "1"
-                          }
-                        ]
-                      },
-                      {
-                        "tag": "nobr"
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "nobr",
-                    "children": [
-                      {
-                        "tag": "br"
-                      },
-                      {
-                        "tag": "a",
-                        "attrs": [
-                          {
-                            "name": "href",
-                            "value": "#2"
-                          }
-                        ]
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "a",
-                    "attrs": [
-                      {
-                        "name": "href",
-                        "value": "#2"
-                      }
-                    ],
-                    "children": [
-                      {
-                        "tag": "nobr",
-                        "children": [
-                          {
-                            "text": "2"
-                          }
-                        ]
-                      },
-                      {
-                        "tag": "nobr"
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "nobr",
-                    "children": [
-                      {
-                        "tag": "br"
-                      },
-                      {
-                        "tag": "a",
-                        "attrs": [
-                          {
-                            "name": "href",
-                            "value": "#3"
-                          }
-                        ]
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "a",
-                    "attrs": [
-                      {
-                        "name": "href",
-                        "value": "#3"
-                      }
-                    ],
-                    "children": [
-                      {
-                        "tag": "nobr",
-                        "children": [
-                          {
-                            "text": "3"
-                          }
-                        ]
-                      },
-                      {
-                        "tag": "nobr"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><a href=\"#1\"><nobr>1</nobr><nobr></nobr></a><nobr><br><a href=\"#2\"></a></nobr><a href=\"#2\"><nobr>2</nobr><nobr></nobr></a><nobr><br><a href=\"#3\"></a></nobr><a href=\"#3\"><nobr>3</nobr><nobr></nobr></a></body></html>",
-        "noQuirksBodyHtml": "<a href=\"#1\"><nobr>1</nobr><nobr></nobr></a><nobr><br><a href=\"#2\"></a></nobr><a href=\"#2\"><nobr>2</nobr><nobr></nobr></a><nobr><br><a href=\"#3\"></a></nobr><a href=\"#3\"><nobr>3</nobr><nobr></nobr></a>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><body><b><nobr>1<nobr></b><i><nobr>2<nobr></i>3",
-      "errors": [
-        "(1,37): unexpected-start-tag-implies-end-tag",
-        "(1,41): adoption-agency-1.3",
-        "(1,50): unexpected-start-tag-implies-end-tag",
-        "(1,50): adoption-agency-1.3",
-        "(1,57): unexpected-start-tag-implies-end-tag",
-        "(1,61): adoption-agency-1.3",
-        "(1,62): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "b": true,
-            "nobr": true,
-            "i": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "b",
-                    "children": [
-                      {
-                        "tag": "nobr",
-                        "children": [
-                          {
-                            "text": "1"
-                          }
-                        ]
-                      },
-                      {
-                        "tag": "nobr"
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "nobr",
-                    "children": [
-                      {
-                        "tag": "i"
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "i",
-                    "children": [
-                      {
-                        "tag": "nobr",
-                        "children": [
-                          {
-                            "text": "2"
-                          }
-                        ]
-                      },
-                      {
-                        "tag": "nobr"
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "nobr",
-                    "children": [
-                      {
-                        "text": "3"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><b><nobr>1</nobr><nobr></nobr></b><nobr><i></i></nobr><i><nobr>2</nobr><nobr></nobr></i><nobr>3</nobr></body></html>",
-        "noQuirksBodyHtml": "<b><nobr>1</nobr><nobr></nobr></b><nobr><i></i></nobr><i><nobr>2</nobr><nobr></nobr></i><nobr>3</nobr>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><body><b><nobr>1<table><nobr></b><i><nobr>2<nobr></i>3",
-      "errors": [
-        "(1,44): foster-parenting-start-tag",
-        "(1,48): foster-parenting-end-tag",
-        "(1,48): adoption-agency-1.3",
-        "(1,51): foster-parenting-start-tag",
-        "(1,57): foster-parenting-start-tag",
-        "(1,57): nobr-already-in-scope",
-        "(1,57): adoption-agency-1.2",
-        "(1,58): foster-parenting-character",
-        "(1,64): foster-parenting-start-tag",
-        "(1,64): nobr-already-in-scope",
-        "(1,68): foster-parenting-end-tag",
-        "(1,68): adoption-agency-1.2",
-        "(1,69): foster-parenting-character",
-        "(1,69): eof-in-table"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "b": true,
-            "nobr": true,
-            "i": true,
-            "table": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "b",
-                    "children": [
-                      {
-                        "tag": "nobr",
-                        "children": [
-                          {
-                            "text": "1"
-                          },
-                          {
-                            "tag": "nobr",
-                            "children": [
-                              {
-                                "tag": "i"
-                              }
-                            ]
-                          },
-                          {
-                            "tag": "i",
-                            "children": [
-                              {
-                                "tag": "nobr",
-                                "children": [
-                                  {
-                                    "text": "2"
-                                  }
-                                ]
-                              },
-                              {
-                                "tag": "nobr"
-                              }
-                            ]
-                          },
-                          {
-                            "tag": "nobr",
-                            "children": [
-                              {
-                                "text": "3"
-                              }
-                            ]
-                          },
-                          {
-                            "tag": "table"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><b><nobr>1<nobr><i></i></nobr><i><nobr>2</nobr><nobr></nobr></i><nobr>3</nobr><table></table></nobr></b></body></html>",
-        "noQuirksBodyHtml": "<b><nobr>1<nobr><i></i></nobr><i><nobr>2</nobr><nobr></nobr></i><nobr>3</nobr><table></table></nobr></b>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><body><b><nobr>1<table><tr><td><nobr></b><i><nobr>2<nobr></i>3",
-      "errors": [
-        "(1,56): unexpected-end-tag",
-        "(1,65): unexpected-start-tag-implies-end-tag",
-        "(1,65): adoption-agency-1.3",
-        "(1,72): unexpected-start-tag-implies-end-tag",
-        "(1,76): adoption-agency-1.3",
-        "(1,77): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "b": true,
-            "nobr": true,
-            "table": true,
-            "tbody": true,
-            "tr": true,
-            "td": true,
-            "i": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "b",
-                    "children": [
-                      {
-                        "tag": "nobr",
-                        "children": [
-                          {
-                            "text": "1"
-                          },
-                          {
-                            "tag": "table",
-                            "children": [
-                              {
-                                "tag": "tbody",
-                                "children": [
-                                  {
-                                    "tag": "tr",
-                                    "children": [
-                                      {
-                                        "tag": "td",
-                                        "children": [
-                                          {
-                                            "tag": "nobr",
-                                            "children": [
-                                              {
-                                                "tag": "i"
-                                              }
-                                            ]
-                                          },
-                                          {
-                                            "tag": "i",
-                                            "children": [
-                                              {
-                                                "tag": "nobr",
-                                                "children": [
-                                                  {
-                                                    "text": "2"
-                                                  }
-                                                ]
-                                              },
-                                              {
-                                                "tag": "nobr"
-                                              }
-                                            ]
-                                          },
-                                          {
-                                            "tag": "nobr",
-                                            "children": [
-                                              {
-                                                "text": "3"
-                                              }
-                                            ]
-                                          }
-                                        ]
-                                      }
-                                    ]
-                                  }
-                                ]
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><b><nobr>1<table><tbody><tr><td><nobr><i></i></nobr><i><nobr>2</nobr><nobr></nobr></i><nobr>3</nobr></td></tr></tbody></table></nobr></b></body></html>",
-        "noQuirksBodyHtml": "<b><nobr>1<table><tbody><tr><td><nobr><i></i></nobr><i><nobr>2</nobr><nobr></nobr></i><nobr>3</nobr></td></tr></tbody></table></nobr></b>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><body><b><nobr>1<div><nobr></b><i><nobr>2<nobr></i>3",
-      "errors": [
-        "(1,42): unexpected-start-tag-implies-end-tag",
-        "(1,42): adoption-agency-1.3",
-        "(1,46): adoption-agency-1.3",
-        "(1,46): adoption-agency-1.3",
-        "(1,55): unexpected-start-tag-implies-end-tag",
-        "(1,55): adoption-agency-1.3",
-        "(1,62): unexpected-start-tag-implies-end-tag",
-        "(1,66): adoption-agency-1.3",
-        "(1,67): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "b": true,
-            "nobr": true,
-            "div": true,
-            "i": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "b",
-                    "children": [
-                      {
-                        "tag": "nobr",
-                        "children": [
-                          {
-                            "text": "1"
-                          }
-                        ]
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "div",
-                    "children": [
-                      {
-                        "tag": "b",
-                        "children": [
-                          {
-                            "tag": "nobr"
-                          },
-                          {
-                            "tag": "nobr"
-                          }
-                        ]
-                      },
-                      {
-                        "tag": "nobr",
-                        "children": [
-                          {
-                            "tag": "i"
-                          }
-                        ]
-                      },
-                      {
-                        "tag": "i",
-                        "children": [
-                          {
-                            "tag": "nobr",
-                            "children": [
-                              {
-                                "text": "2"
-                              }
-                            ]
-                          },
-                          {
-                            "tag": "nobr"
-                          }
-                        ]
-                      },
-                      {
-                        "tag": "nobr",
-                        "children": [
-                          {
-                            "text": "3"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><b><nobr>1</nobr></b><div><b><nobr></nobr><nobr></nobr></b><nobr><i></i></nobr><i><nobr>2</nobr><nobr></nobr></i><nobr>3</nobr></div></body></html>",
-        "noQuirksBodyHtml": "<b><nobr>1</nobr></b><div><b><nobr></nobr><nobr></nobr></b><nobr><i></i></nobr><i><nobr>2</nobr><nobr></nobr></i><nobr>3</nobr></div>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><body><b><nobr>1<nobr></b><div><i><nobr>2<nobr></i>3",
-      "errors": [
-        "(1,37): unexpected-start-tag-implies-end-tag",
-        "(1,41): adoption-agency-1.3",
-        "(1,55): unexpected-start-tag-implies-end-tag",
-        "(1,55): adoption-agency-1.3",
-        "(1,62): unexpected-start-tag-implies-end-tag",
-        "(1,66): adoption-agency-1.3",
-        "(1,67): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "b": true,
-            "nobr": true,
-            "div": true,
-            "i": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "b",
-                    "children": [
-                      {
-                        "tag": "nobr",
-                        "children": [
-                          {
-                            "text": "1"
-                          }
-                        ]
-                      },
-                      {
-                        "tag": "nobr"
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "div",
-                    "children": [
-                      {
-                        "tag": "nobr",
-                        "children": [
-                          {
-                            "tag": "i"
-                          }
-                        ]
-                      },
-                      {
-                        "tag": "i",
-                        "children": [
-                          {
-                            "tag": "nobr",
-                            "children": [
-                              {
-                                "text": "2"
-                              }
-                            ]
-                          },
-                          {
-                            "tag": "nobr"
-                          }
-                        ]
-                      },
-                      {
-                        "tag": "nobr",
-                        "children": [
-                          {
-                            "text": "3"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><b><nobr>1</nobr><nobr></nobr></b><div><nobr><i></i></nobr><i><nobr>2</nobr><nobr></nobr></i><nobr>3</nobr></div></body></html>",
-        "noQuirksBodyHtml": "<b><nobr>1</nobr><nobr></nobr></b><div><nobr><i></i></nobr><i><nobr>2</nobr><nobr></nobr></i><nobr>3</nobr></div>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><body><b><nobr>1<nobr><ins></b><i><nobr>",
-      "errors": [
-        "(1,37): unexpected-start-tag-implies-end-tag",
-        "(1,46): adoption-agency-1.3",
-        "(1,55): unexpected-start-tag-implies-end-tag",
-        "(1,55): adoption-agency-1.3",
-        "(1,55): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "b": true,
-            "nobr": true,
-            "ins": true,
-            "i": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "b",
-                    "children": [
-                      {
-                        "tag": "nobr",
-                        "children": [
-                          {
-                            "text": "1"
-                          }
-                        ]
-                      },
-                      {
-                        "tag": "nobr",
-                        "children": [
-                          {
-                            "tag": "ins"
-                          }
-                        ]
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "nobr",
-                    "children": [
-                      {
-                        "tag": "i"
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "i",
-                    "children": [
-                      {
-                        "tag": "nobr"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><b><nobr>1</nobr><nobr><ins></ins></nobr></b><nobr><i></i></nobr><i><nobr></nobr></i></body></html>",
-        "noQuirksBodyHtml": "<b><nobr>1</nobr><nobr><ins></ins></nobr></b><nobr><i></i></nobr><i><nobr></nobr></i>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><body><b><nobr>1<ins><nobr></b><i>2",
-      "errors": [
-        "(1,42): unexpected-start-tag-implies-end-tag",
-        "(1,42): adoption-agency-1.3",
-        "(1,46): adoption-agency-1.3",
-        "(1,50): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "b": true,
-            "nobr": true,
-            "ins": true,
-            "i": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "b",
-                    "children": [
-                      {
-                        "tag": "nobr",
-                        "children": [
-                          {
-                            "text": "1"
-                          },
-                          {
-                            "tag": "ins"
-                          }
-                        ]
-                      },
-                      {
-                        "tag": "nobr"
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "nobr",
-                    "children": [
-                      {
-                        "tag": "i",
-                        "children": [
-                          {
-                            "text": "2"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><b><nobr>1<ins></ins></nobr><nobr></nobr></b><nobr><i>2</i></nobr></body></html>",
-        "noQuirksBodyHtml": "<b><nobr>1<ins></ins></nobr><nobr></nobr></b><nobr><i>2</i></nobr>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><body><b>1<nobr></b><i><nobr>2</i>",
-      "errors": [
-        "(1,35): adoption-agency-1.3",
-        "(1,44): unexpected-start-tag-implies-end-tag",
-        "(1,44): adoption-agency-1.3",
-        "(1,49): adoption-agency-1.3"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "b": true,
-            "nobr": true,
-            "i": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "b",
-                    "children": [
-                      {
-                        "text": "1"
-                      },
-                      {
-                        "tag": "nobr"
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "nobr",
-                    "children": [
-                      {
-                        "tag": "i"
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "i",
-                    "children": [
-                      {
-                        "tag": "nobr",
-                        "children": [
-                          {
-                            "text": "2"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><b>1<nobr></nobr></b><nobr><i></i></nobr><i><nobr>2</nobr></i></body></html>",
-        "noQuirksBodyHtml": "<b>1<nobr></nobr></b><nobr><i></i></nobr><i><nobr>2</nobr></i>"
-      }
-    },
-    {
-      "data": "<p><code x</code></p>\n",
-      "errors": [
-        "(1,3): expected-doctype-but-got-start-tag",
-        "(1,11): invalid-character-in-attribute-name",
-        "(1,12): unexpected-character-after-solidus-in-tag",
-        "(1,21): unexpected-end-tag",
-        "(2,0): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "p": true,
-            "code": true
-          },
-          "attrWithFunnyChar": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "p",
-                    "children": [
-                      {
-                        "tag": "code",
-                        "attrs": [
-                          {
-                            "name": "code",
-                            "value": ""
-                          },
-                          {
-                            "name": "x<",
-                            "value": ""
-                          }
-                        ]
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "code",
-                    "attrs": [
-                      {
-                        "name": "code",
-                        "value": ""
-                      },
-                      {
-                        "name": "x<",
-                        "value": ""
-                      }
-                    ],
-                    "children": [
-                      {
-                        "text": "\n"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><p><code x<=\"\" code=\"\"></code></p><code x<=\"\" code=\"\">\n</code></body></html>",
-        "noQuirksBodyHtml": "<p><code x<=\"\" code=\"\"></code></p><code x<=\"\" code=\"\">\n</code>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><svg><foreignObject><p><i></p>a",
-      "errors": [
-        "(1,45): unexpected-end-tag",
-        "(1,46): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "svg svg": true,
-            "svg foreignObject": true,
-            "p": true,
-            "i": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "svg",
-                    "ns": "http://www.w3.org/2000/svg",
-                    "children": [
-                      {
-                        "tag": "foreignObject",
-                        "ns": "http://www.w3.org/2000/svg",
-                        "children": [
-                          {
-                            "tag": "p",
-                            "children": [
-                              {
-                                "tag": "i"
-                              }
-                            ]
-                          },
-                          {
-                            "tag": "i",
-                            "children": [
-                              {
-                                "text": "a"
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><svg><foreignObject><p><i></i></p><i>a</i></foreignObject></svg></body></html>",
-        "noQuirksBodyHtml": "<svg><foreignObject><p><i></i></p><i>a</i></foreignObject></svg>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><table><tr><td><svg><foreignObject><p><i></p>a",
-      "errors": [
-        "(1,60): unexpected-end-tag",
-        "(1,61): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true,
-            "tbody": true,
-            "tr": true,
-            "td": true,
-            "svg svg": true,
-            "svg foreignObject": true,
-            "p": true,
-            "i": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "tbody",
-                        "children": [
-                          {
-                            "tag": "tr",
-                            "children": [
-                              {
-                                "tag": "td",
-                                "children": [
-                                  {
-                                    "tag": "svg",
-                                    "ns": "http://www.w3.org/2000/svg",
-                                    "children": [
-                                      {
-                                        "tag": "foreignObject",
-                                        "ns": "http://www.w3.org/2000/svg",
-                                        "children": [
-                                          {
-                                            "tag": "p",
-                                            "children": [
-                                              {
-                                                "tag": "i"
-                                              }
-                                            ]
-                                          },
-                                          {
-                                            "tag": "i",
-                                            "children": [
-                                              {
-                                                "text": "a"
-                                              }
-                                            ]
-                                          }
-                                        ]
-                                      }
-                                    ]
-                                  }
-                                ]
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><table><tbody><tr><td><svg><foreignObject><p><i></i></p><i>a</i></foreignObject></svg></td></tr></tbody></table></body></html>",
-        "noQuirksBodyHtml": "<table><tbody><tr><td><svg><foreignObject><p><i></i></p><i>a</i></foreignObject></svg></td></tr></tbody></table>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><math><mtext><p><i></p>a",
-      "errors": [
-        "(1,38): unexpected-end-tag",
-        "(1,39): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "math math": true,
-            "math mtext": true,
-            "p": true,
-            "i": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "math",
-                    "ns": "http://www.w3.org/1998/Math/MathML",
-                    "children": [
-                      {
-                        "tag": "mtext",
-                        "ns": "http://www.w3.org/1998/Math/MathML",
-                        "children": [
-                          {
-                            "tag": "p",
-                            "children": [
-                              {
-                                "tag": "i"
-                              }
-                            ]
-                          },
-                          {
-                            "tag": "i",
-                            "children": [
-                              {
-                                "text": "a"
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><math><mtext><p><i></i></p><i>a</i></mtext></math></body></html>",
-        "noQuirksBodyHtml": "<math><mtext><p><i></i></p><i>a</i></mtext></math>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><table><tr><td><math><mtext><p><i></p>a",
-      "errors": [
-        "(1,53): unexpected-end-tag",
-        "(1,54): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true,
-            "tbody": true,
-            "tr": true,
-            "td": true,
-            "math math": true,
-            "math mtext": true,
-            "p": true,
-            "i": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "tbody",
-                        "children": [
-                          {
-                            "tag": "tr",
-                            "children": [
-                              {
-                                "tag": "td",
-                                "children": [
-                                  {
-                                    "tag": "math",
-                                    "ns": "http://www.w3.org/1998/Math/MathML",
-                                    "children": [
-                                      {
-                                        "tag": "mtext",
-                                        "ns": "http://www.w3.org/1998/Math/MathML",
-                                        "children": [
-                                          {
-                                            "tag": "p",
-                                            "children": [
-                                              {
-                                                "tag": "i"
-                                              }
-                                            ]
-                                          },
-                                          {
-                                            "tag": "i",
-                                            "children": [
-                                              {
-                                                "text": "a"
-                                              }
-                                            ]
-                                          }
-                                        ]
-                                      }
-                                    ]
-                                  }
-                                ]
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><table><tbody><tr><td><math><mtext><p><i></i></p><i>a</i></mtext></math></td></tr></tbody></table></body></html>",
-        "noQuirksBodyHtml": "<table><tbody><tr><td><math><mtext><p><i></i></p><i>a</i></mtext></math></td></tr></tbody></table>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><body><div><!/div>a",
-      "errors": [
-        "(1,28): expected-dashes-or-doctype",
-        "(1,34): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "div": true
-          },
-          "doctype": true,
-          "comment": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "div",
-                    "children": [
-                      {
-                        "comment": "/div"
-                      },
-                      {
-                        "text": "a"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><div><!--/div-->a</div></body></html>",
-        "noQuirksBodyHtml": "<div><!--/div-->a</div>"
-      }
-    },
-    {
-      "data": "<button><p><button>",
-      "errors": [
-        "Line 1 Col 8 Unexpected start tag (button). Expected DOCTYPE.",
-        "Line 1 Col 19 Unexpected start tag (button) implies end tag (button).",
-        "Line 1 Col 19 Expected closing tag. Unexpected end of file."
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "button": true,
-            "p": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "button",
-                    "children": [
-                      {
-                        "tag": "p"
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "button"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><button><p></p></button><button></button></body></html>",
-        "noQuirksBodyHtml": "<button><p></p></button><button></button>"
-      }
-    }
-  ],
-  "tests3.dat": [
-    {
-      "data": "<head></head><style></style>",
-      "errors": [
-        "(1,6): expected-doctype-but-got-start-tag",
-        "(1,20): unexpected-start-tag-out-of-my-head"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "style": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "style"
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><style></style></head><body></body></html>",
-        "noQuirksBodyHtml": "<style></style>"
-      }
-    },
-    {
-      "data": "<head></head><script></script>",
-      "errors": [
-        "(1,6): expected-doctype-but-got-start-tag",
-        "(1,21): unexpected-start-tag-out-of-my-head"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script"
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><script></script></head><body></body></html>",
-        "noQuirksBodyHtml": "<script></script>"
-      }
-    },
-    {
-      "data": "<head></head><!-- --><style></style><!-- --><script></script>",
-      "errors": [
-        "(1,6): expected-doctype-but-got-start-tag",
-        "(1,28): unexpected-start-tag-out-of-my-head",
-        "(1,52): unexpected-start-tag-out-of-my-head"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "style": true,
-            "script": true,
-            "body": true
-          },
-          "comment": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "style"
-                  },
-                  {
-                    "tag": "script"
-                  }
-                ]
-              },
-              {
-                "comment": " "
-              },
-              {
-                "comment": " "
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><style></style><script></script></head><!-- --><!-- --><body></body></html>",
-        "noQuirksBodyHtml": "<!-- --><style></style><!-- --><script></script>"
-      }
-    },
-    {
-      "data": "<head></head><!-- -->x<style></style><!-- --><script></script>",
-      "errors": [
-        "(1,6): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "style": true,
-            "script": true
-          },
-          "comment": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "comment": " "
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "x"
-                  },
-                  {
-                    "tag": "style"
-                  },
-                  {
-                    "comment": " "
-                  },
-                  {
-                    "tag": "script"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><!-- --><body>x<style></style><!-- --><script></script></body></html>",
-        "noQuirksBodyHtml": "<!-- -->x<style></style><!-- --><script></script>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><html><head></head><body><pre>\n</pre></body></html>",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "pre": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "pre"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><pre></pre></body></html>",
-        "noQuirksBodyHtml": "<pre></pre>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><html><head></head><body><pre>\nfoo</pre></body></html>",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "pre": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "pre",
-                    "children": [
-                      {
-                        "text": "foo"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><pre>foo</pre></body></html>",
-        "noQuirksBodyHtml": "<pre>foo</pre>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><html><head></head><body><pre>\n\nfoo</pre></body></html>",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "pre": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "pre",
-                    "children": [
-                      {
-                        "text": "\nfoo"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><pre>\nfoo</pre></body></html>",
-        "noQuirksBodyHtml": "<pre>\nfoo</pre>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><html><head></head><body><pre>\nfoo\n</pre></body></html>",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "pre": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "pre",
-                    "children": [
-                      {
-                        "text": "foo\n"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><pre>foo\n</pre></body></html>",
-        "noQuirksBodyHtml": "<pre>foo\n</pre>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><html><head></head><body><pre>x</pre><span>\n</span></body></html>",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "pre": true,
-            "span": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "pre",
-                    "children": [
-                      {
-                        "text": "x"
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "span",
-                    "children": [
-                      {
-                        "text": "\n"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><pre>x</pre><span>\n</span></body></html>",
-        "noQuirksBodyHtml": "<pre>x</pre><span>\n</span>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><html><head></head><body><pre>x\ny</pre></body></html>",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "pre": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "pre",
-                    "children": [
-                      {
-                        "text": "x\ny"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><pre>x\ny</pre></body></html>",
-        "noQuirksBodyHtml": "<pre>x\ny</pre>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><html><head></head><body><pre>x<div>\ny</pre></body></html>",
-      "errors": [
-        "(2,7): end-tag-too-early"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "pre": true,
-            "div": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "pre",
-                    "children": [
-                      {
-                        "text": "x"
-                      },
-                      {
-                        "tag": "div",
-                        "children": [
-                          {
-                            "text": "\ny"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><pre>x<div>\ny</div></pre></body></html>",
-        "noQuirksBodyHtml": "<pre>x<div>\ny</div></pre>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><pre>&#x0a;&#x0a;A</pre>",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "pre": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "pre",
-                    "children": [
-                      {
-                        "text": "\nA"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><pre>\nA</pre></body></html>",
-        "noQuirksBodyHtml": "<pre>\nA</pre>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><HTML><META><HEAD></HEAD></HTML>",
-      "errors": [
-        "(1,33): two-heads-are-not-better-than-one"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "meta": true,
-            "body": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "meta"
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head><meta></head><body></body></html>",
-        "noQuirksBodyHtml": "<meta>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><HTML><HEAD><head></HEAD></HTML>",
-      "errors": [
-        "(1,33): two-heads-are-not-better-than-one"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body></body></html>",
-        "noQuirksBodyHtml": ""
-      }
-    },
-    {
-      "data": "<textarea>foo<span>bar</span><i>baz",
-      "errors": [
-        "(1,10): expected-doctype-but-got-start-tag",
-        "(1,35): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "textarea": true
-          },
-          "escaped": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "textarea",
-                    "children": [
-                      {
-                        "text": "foo<span>bar</span><i>baz",
-                        "escaped": true
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><textarea>foo&lt;span&gt;bar&lt;/span&gt;&lt;i&gt;baz</textarea></body></html>",
-        "noQuirksBodyHtml": "<textarea>foo&lt;span&gt;bar&lt;/span&gt;&lt;i&gt;baz</textarea>"
-      }
-    },
-    {
-      "data": "<title>foo<span>bar</em><i>baz",
-      "errors": [
-        "(1,7): expected-doctype-but-got-start-tag",
-        "(1,30): expected-named-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "title": true,
-            "body": true
-          },
-          "escaped": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "title",
-                    "children": [
-                      {
-                        "text": "foo<span>bar</em><i>baz",
-                        "escaped": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><title>foo&lt;span&gt;bar&lt;/em&gt;&lt;i&gt;baz</title></head><body></body></html>",
-        "noQuirksBodyHtml": "<title>foo&lt;span&gt;bar&lt;/em&gt;&lt;i&gt;baz</title>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><textarea>\n</textarea>",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "textarea": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "textarea"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><textarea></textarea></body></html>",
-        "noQuirksBodyHtml": "<textarea></textarea>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><textarea>\nfoo</textarea>",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "textarea": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "textarea",
-                    "children": [
-                      {
-                        "text": "foo"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><textarea>foo</textarea></body></html>",
-        "noQuirksBodyHtml": "<textarea>foo</textarea>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><textarea>\n\nfoo</textarea>",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "textarea": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "textarea",
-                    "children": [
-                      {
-                        "text": "\nfoo"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><textarea>\nfoo</textarea></body></html>",
-        "noQuirksBodyHtml": "<textarea>\nfoo</textarea>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><html><head></head><body><ul><li><div><p><li></ul></body></html>",
-      "errors": [
-        "(1,60): end-tag-too-early"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "ul": true,
-            "li": true,
-            "div": true,
-            "p": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "ul",
-                    "children": [
-                      {
-                        "tag": "li",
-                        "children": [
-                          {
-                            "tag": "div",
-                            "children": [
-                              {
-                                "tag": "p"
-                              }
-                            ]
-                          }
-                        ]
-                      },
-                      {
-                        "tag": "li"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><ul><li><div><p></p></div></li><li></li></ul></body></html>",
-        "noQuirksBodyHtml": "<ul><li><div><p></p></div></li><li></li></ul>"
-      }
-    },
-    {
-      "data": "<!doctype html><nobr><nobr><nobr>",
-      "errors": [
-        "(1,27): unexpected-start-tag-implies-end-tag",
-        "(1,33): unexpected-start-tag-implies-end-tag",
-        "(1,33): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "nobr": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "nobr"
-                  },
-                  {
-                    "tag": "nobr"
-                  },
-                  {
-                    "tag": "nobr"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><nobr></nobr><nobr></nobr><nobr></nobr></body></html>",
-        "noQuirksBodyHtml": "<nobr></nobr><nobr></nobr><nobr></nobr>"
-      }
-    },
-    {
-      "data": "<!doctype html><nobr><nobr></nobr><nobr>",
-      "errors": [
-        "(1,27): unexpected-start-tag-implies-end-tag",
-        "(1,40): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "nobr": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "nobr"
-                  },
-                  {
-                    "tag": "nobr"
-                  },
-                  {
-                    "tag": "nobr"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><nobr></nobr><nobr></nobr><nobr></nobr></body></html>",
-        "noQuirksBodyHtml": "<nobr></nobr><nobr></nobr><nobr></nobr>"
-      }
-    },
-    {
-      "data": "<!doctype html><html><body><p><table></table></body></html>",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "p": true,
-            "table": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "p"
-                  },
-                  {
-                    "tag": "table"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><p></p><table></table></body></html>",
-        "noQuirksBodyHtml": "<p></p><table></table>"
-      }
-    },
-    {
-      "data": "<p><table></table>",
-      "errors": [
-        "(1,3): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "p": true,
-            "table": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "p",
-                    "children": [
-                      {
-                        "tag": "table"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><p><table></table></p></body></html>",
-        "noQuirksBodyHtml": "<p></p><table></table>"
-      }
-    }
-  ],
-  "tests4.dat": [
-    {
-      "data": "direct div content",
-      "errors": [],
-      "fragment": {
-        "name": "div"
-      },
-      "document": {
-        "props": {
-          "tags": {}
-        },
-        "tree": [
-          {
-            "text": "direct div content"
-          }
-        ],
-        "html": "direct div content",
-        "noQuirksBodyHtml": "direct div content"
-      }
-    },
-    {
-      "data": "direct textarea content",
-      "errors": [],
-      "fragment": {
-        "name": "textarea"
-      },
-      "document": {
-        "props": {
-          "tags": {}
-        },
-        "tree": [
-          {
-            "text": "direct textarea content"
-          }
-        ],
-        "html": "direct textarea content",
-        "noQuirksBodyHtml": "direct textarea content"
-      }
-    },
-    {
-      "data": "textarea content with <em>pseudo</em> <foo>markup",
-      "errors": [],
-      "fragment": {
-        "name": "textarea"
-      },
-      "document": {
-        "props": {
-          "tags": {},
-          "escaped": true
-        },
-        "tree": [
-          {
-            "text": "textarea content with <em>pseudo</em> <foo>markup",
-            "escaped": true
-          }
-        ],
-        "html": "textarea content with &lt;em&gt;pseudo&lt;/em&gt; &lt;foo&gt;markup",
-        "noQuirksBodyHtml": "textarea content with <em>pseudo</em> <foo>markup</foo>"
-      }
-    },
-    {
-      "data": "this is &#x0043;DATA inside a <style> element",
-      "errors": [],
-      "fragment": {
-        "name": "style"
-      },
-      "document": {
-        "props": {
-          "tags": {},
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "text": "this is &#x0043;DATA inside a <style> element",
-            "no_escape": true
-          }
-        ],
-        "html": "this is &#x0043;DATA inside a <style> element",
-        "noQuirksBodyHtml": "this is CDATA inside a <style> element</style>"
-      }
-    },
-    {
-      "data": "</plaintext>",
-      "errors": [],
-      "fragment": {
-        "name": "plaintext"
-      },
-      "document": {
-        "props": {
-          "tags": {},
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "text": "</plaintext>",
-            "no_escape": true
-          }
-        ],
-        "html": "</plaintext>",
-        "noQuirksBodyHtml": ""
-      }
-    },
-    {
-      "data": "setting html's innerHTML",
-      "errors": [],
-      "fragment": {
-        "name": "html"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "head": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "head"
-          },
-          {
-            "tag": "body",
-            "children": [
-              {
-                "text": "setting html's innerHTML"
-              }
-            ]
-          }
-        ],
-        "html": "<head></head><body>setting html's innerHTML</body>",
-        "noQuirksBodyHtml": "setting html's innerHTML"
-      }
-    },
-    {
-      "data": "<title>setting head's innerHTML</title>",
-      "errors": [],
-      "fragment": {
-        "name": "head"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "title": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "title",
-            "children": [
-              {
-                "text": "setting head's innerHTML"
-              }
-            ]
-          }
-        ],
-        "html": "<title>setting head's innerHTML</title>",
-        "noQuirksBodyHtml": "<title>setting head's innerHTML</title>"
-      }
-    }
-  ],
-  "tests5.dat": [
-    {
-      "data": "<style> <!-- </style>x",
-      "errors": [
-        "(1,7): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "style": true,
-            "body": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "style",
-                    "children": [
-                      {
-                        "text": " <!-- ",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "x"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><style> <!-- </style></head><body>x</body></html>",
-        "noQuirksBodyHtml": "<style> <!-- </style>x"
-      }
-    },
-    {
-      "data": "<style> <!-- </style> --> </style>x",
-      "errors": [
-        "(1,7): expected-doctype-but-got-start-tag",
-        "(1,34): unexpected-end-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "style": true,
-            "body": true
-          },
-          "no_escape": true,
-          "escaped": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "style",
-                    "children": [
-                      {
-                        "text": " <!-- ",
-                        "no_escape": true
-                      }
-                    ]
-                  },
-                  {
-                    "text": " "
-                  }
-                ]
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "--> x",
-                    "escaped": true
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><style> <!-- </style> </head><body>--&gt; x</body></html>",
-        "noQuirksBodyHtml": "<style> <!-- </style> --&gt; x"
-      }
-    },
-    {
-      "data": "<style> <!--> </style>x",
-      "errors": [
-        "(1,7): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "style": true,
-            "body": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "style",
-                    "children": [
-                      {
-                        "text": " <!--> ",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "x"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><style> <!--> </style></head><body>x</body></html>",
-        "noQuirksBodyHtml": "<style> <!--> </style>x"
-      }
-    },
-    {
-      "data": "<style> <!---> </style>x",
-      "errors": [
-        "(1,7): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "style": true,
-            "body": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "style",
-                    "children": [
-                      {
-                        "text": " <!---> ",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "x"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><style> <!---> </style></head><body>x</body></html>",
-        "noQuirksBodyHtml": "<style> <!---> </style>x"
-      }
-    },
-    {
-      "data": "<iframe> <!---> </iframe>x",
-      "errors": [
-        "(1,8): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "iframe": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "iframe",
-                    "children": [
-                      {
-                        "text": " <!---> ",
-                        "no_escape": true
-                      }
-                    ]
-                  },
-                  {
-                    "text": "x"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><iframe> <!---> </iframe>x</body></html>",
-        "noQuirksBodyHtml": "<iframe> <!---> </iframe>x"
-      }
-    },
-    {
-      "data": "<iframe> <!--- </iframe>->x</iframe> --> </iframe>x",
-      "errors": [
-        "(1,8): expected-doctype-but-got-start-tag",
-        "(1,36): unexpected-end-tag",
-        "(1,50): unexpected-end-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "iframe": true
-          },
-          "no_escape": true,
-          "escaped": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "iframe",
-                    "children": [
-                      {
-                        "text": " <!--- ",
-                        "no_escape": true
-                      }
-                    ]
-                  },
-                  {
-                    "text": "->x --> x",
-                    "escaped": true
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><iframe> <!--- </iframe>-&gt;x --&gt; x</body></html>",
-        "noQuirksBodyHtml": "<iframe> <!--- </iframe>-&gt;x --&gt; x"
-      }
-    },
-    {
-      "data": "<script> <!-- </script> --> </script>x",
-      "errors": [
-        "(1,8): expected-doctype-but-got-start-tag",
-        "(1,37): unexpected-end-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "script": true,
-            "body": true
-          },
-          "no_escape": true,
-          "escaped": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": " <!-- ",
-                        "no_escape": true
-                      }
-                    ]
-                  },
-                  {
-                    "text": " "
-                  }
-                ]
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "--> x",
-                    "escaped": true
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><script> <!-- </script> </head><body>--&gt; x</body></html>",
-        "noQuirksBodyHtml": "<script> <!-- </script> --&gt; x"
-      }
-    },
-    {
-      "data": "<title> <!-- </title> --> </title>x",
-      "errors": [
-        "(1,7): expected-doctype-but-got-start-tag",
-        "(1,34): unexpected-end-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "title": true,
-            "body": true
-          },
-          "escaped": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "title",
-                    "children": [
-                      {
-                        "text": " <!-- ",
-                        "escaped": true
-                      }
-                    ]
-                  },
-                  {
-                    "text": " "
-                  }
-                ]
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "--> x",
-                    "escaped": true
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><title> &lt;!-- </title> </head><body>--&gt; x</body></html>",
-        "noQuirksBodyHtml": "<title> &lt;!-- </title> --&gt; x"
-      }
-    },
-    {
-      "data": "<textarea> <!--- </textarea>->x</textarea> --> </textarea>x",
-      "errors": [
-        "(1,10): expected-doctype-but-got-start-tag",
-        "(1,42): unexpected-end-tag",
-        "(1,58): unexpected-end-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "textarea": true
-          },
-          "escaped": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "textarea",
-                    "children": [
-                      {
-                        "text": " <!--- ",
-                        "escaped": true
-                      }
-                    ]
-                  },
-                  {
-                    "text": "->x --> x",
-                    "escaped": true
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><textarea> &lt;!--- </textarea>-&gt;x --&gt; x</body></html>",
-        "noQuirksBodyHtml": "<textarea> &lt;!--- </textarea>-&gt;x --&gt; x"
-      }
-    },
-    {
-      "data": "<style> <!</-- </style>x",
-      "errors": [
-        "(1,7): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "style": true,
-            "body": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "style",
-                    "children": [
-                      {
-                        "text": " <!</-- ",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "x"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><style> <!</-- </style></head><body>x</body></html>",
-        "noQuirksBodyHtml": "<style> <!</-- </style>x"
-      }
-    },
-    {
-      "data": "<p><xmp></xmp>",
-      "errors": [
-        "(1,3): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "p": true,
-            "xmp": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "p"
-                  },
-                  {
-                    "tag": "xmp"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><p></p><xmp></xmp></body></html>",
-        "noQuirksBodyHtml": "<p></p><xmp></xmp>"
-      }
-    },
-    {
-      "data": "<xmp> <!-- > --> </xmp>",
-      "errors": [
-        "(1,5): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "xmp": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "xmp",
-                    "children": [
-                      {
-                        "text": " <!-- > --> ",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><xmp> <!-- > --> </xmp></body></html>",
-        "noQuirksBodyHtml": "<xmp> <!-- > --> </xmp>"
-      }
-    },
-    {
-      "data": "<title>&amp;</title>",
-      "errors": [
-        "(1,7): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "title": true,
-            "body": true
-          },
-          "escaped": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "title",
-                    "children": [
-                      {
-                        "text": "&",
-                        "escaped": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><title>&amp;</title></head><body></body></html>",
-        "noQuirksBodyHtml": "<title>&amp;</title>"
-      }
-    },
-    {
-      "data": "<title><!--&amp;--></title>",
-      "errors": [
-        "(1,7): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "title": true,
-            "body": true
-          },
-          "escaped": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "title",
-                    "children": [
-                      {
-                        "text": "<!--&-->",
-                        "escaped": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><title>&lt;!--&amp;--&gt;</title></head><body></body></html>",
-        "noQuirksBodyHtml": "<title>&lt;!--&amp;--&gt;</title>"
-      }
-    },
-    {
-      "data": "<title><!--</title>",
-      "errors": [
-        "(1,7): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "title": true,
-            "body": true
-          },
-          "escaped": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "title",
-                    "children": [
-                      {
-                        "text": "<!--",
-                        "escaped": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><title>&lt;!--</title></head><body></body></html>",
-        "noQuirksBodyHtml": "<title>&lt;!--</title>"
-      }
-    },
-    {
-      "data": "<noscript><!--</noscript>--></noscript>",
-      "errors": [
-        "(1,10): expected-doctype-but-got-start-tag",
-        "(1,39): unexpected-end-tag"
-      ],
-      "script": "on",
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "noscript": true,
-            "body": true
-          },
-          "no_escape": true,
-          "escaped": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "noscript",
-                    "children": [
-                      {
-                        "text": "<!--",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "-->",
-                    "escaped": true
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><noscript><!--</noscript></head><body>--&gt;</body></html>",
-        "noQuirksBodyHtml": "<noscript><!--</noscript>--></noscript>"
-      }
-    },
-    {
-      "data": "<noscript><!--</noscript>--></noscript>",
-      "errors": [
-        "(1,10): expected-doctype-but-got-start-tag"
-      ],
-      "script": "off",
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "noscript": true,
-            "body": true
-          },
-          "comment": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "noscript",
-                    "children": [
-                      {
-                        "comment": "</noscript>"
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><noscript><!--</noscript>--></noscript></head><body></body></html>",
-        "noQuirksBodyHtml": "<noscript><!--</noscript>--></noscript>"
-      }
-    }
-  ],
-  "tests6.dat": [
-    {
-      "data": "<!doctype html></head> <head>",
-      "errors": [
-        "(1,29): unexpected-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "text": " "
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head> <body></body></html>",
-        "noQuirksBodyHtml": " "
-      }
-    },
-    {
-      "data": "<!doctype html><form><div></form><div>",
-      "errors": [
-        "(1,33): end-tag-too-early-ignored",
-        "(1,38): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "form": true,
-            "div": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "form",
-                    "children": [
-                      {
-                        "tag": "div",
-                        "children": [
-                          {
-                            "tag": "div"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><form><div><div></div></div></form></body></html>",
-        "noQuirksBodyHtml": "<form><div><div></div></div></form>"
-      }
-    },
-    {
-      "data": "<!doctype html><title>&amp;</title>",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "title": true,
-            "body": true
-          },
-          "doctype": true,
-          "escaped": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "title",
-                    "children": [
-                      {
-                        "text": "&",
-                        "escaped": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head><title>&amp;</title></head><body></body></html>",
-        "noQuirksBodyHtml": "<title>&amp;</title>"
-      }
-    },
-    {
-      "data": "<!doctype html><title><!--&amp;--></title>",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "title": true,
-            "body": true
-          },
-          "doctype": true,
-          "escaped": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "title",
-                    "children": [
-                      {
-                        "text": "<!--&-->",
-                        "escaped": true
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head><title>&lt;!--&amp;--&gt;</title></head><body></body></html>",
-        "noQuirksBodyHtml": "<title>&lt;!--&amp;--&gt;</title>"
-      }
-    },
-    {
-      "data": "<!doctype>",
-      "errors": [
-        "(1,9): need-space-after-doctype",
-        "(1,10): expected-doctype-name-but-got-right-bracket",
-        "(1,10): unknown-doctype"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": ""
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE ><html><head></head><body></body></html>",
-        "noQuirksBodyHtml": ""
-      }
-    },
-    {
-      "data": "<!---x",
-      "errors": [
-        "(1,6): eof-in-comment",
-        "(1,6): expected-doctype-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "comment": true
-        },
-        "tree": [
-          {
-            "comment": "-x"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!---x--><html><head></head><body></body></html>",
-        "noQuirksBodyHtml": "<!---x-->"
-      }
-    },
-    {
-      "data": "<body>\n<div>",
-      "errors": [
-        "(1,6): unexpected-start-tag",
-        "(2,5): expected-closing-tag-but-got-eof"
-      ],
-      "fragment": {
-        "name": "div"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "div": true
-          }
-        },
-        "tree": [
-          {
-            "text": "\n"
-          },
-          {
-            "tag": "div"
-          }
-        ],
-        "html": "\n<div></div>",
-        "noQuirksBodyHtml": "\n<div></div>"
-      }
-    },
-    {
-      "data": "<frameset></frameset>\nfoo",
-      "errors": [
-        "(1,10): expected-doctype-but-got-start-tag",
-        "(2,1): unexpected-char-after-frameset",
-        "(2,2): unexpected-char-after-frameset",
-        "(2,3): unexpected-char-after-frameset"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "frameset": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "frameset"
-              },
-              {
-                "text": "\n"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><frameset></frameset>\n</html>",
-        "noQuirksBodyHtml": "\nfoo"
-      }
-    },
-    {
-      "data": "<frameset></frameset>\n<noframes>",
-      "errors": [
-        "(1,10): expected-doctype-but-got-start-tag",
-        "(2,10): expected-named-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "frameset": true,
-            "noframes": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "frameset"
-              },
-              {
-                "text": "\n"
-              },
-              {
-                "tag": "noframes"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><frameset></frameset>\n<noframes></noframes></html>",
-        "noQuirksBodyHtml": "\n<noframes></noframes>"
-      }
-    },
-    {
-      "data": "<frameset></frameset>\n<div>",
-      "errors": [
-        "(1,10): expected-doctype-but-got-start-tag",
-        "(2,5): unexpected-start-tag-after-frameset"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "frameset": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "frameset"
-              },
-              {
-                "text": "\n"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><frameset></frameset>\n</html>",
-        "noQuirksBodyHtml": "\n<div></div>"
-      }
-    },
-    {
-      "data": "<frameset></frameset>\n</html>",
-      "errors": [
-        "(1,10): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "frameset": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "frameset"
-              },
-              {
-                "text": "\n"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><frameset></frameset>\n</html>",
-        "noQuirksBodyHtml": "\n"
-      }
-    },
-    {
-      "data": "<frameset></frameset>\n</div>",
-      "errors": [
-        "(1,10): expected-doctype-but-got-start-tag",
-        "(2,6): unexpected-end-tag-after-frameset"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "frameset": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "frameset"
-              },
-              {
-                "text": "\n"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><frameset></frameset>\n</html>",
-        "noQuirksBodyHtml": "\n"
-      }
-    },
-    {
-      "data": "<form><form>",
-      "errors": [
-        "(1,6): expected-doctype-but-got-start-tag",
-        "(1,12): unexpected-start-tag",
-        "(1,12): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "form": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "form"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><form></form></body></html>",
-        "noQuirksBodyHtml": "<form></form>"
-      }
-    },
-    {
-      "data": "<button><button>",
-      "errors": [
-        "(1,8): expected-doctype-but-got-start-tag",
-        "(1,16): unexpected-start-tag-implies-end-tag",
-        "(1,16): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "button": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "button"
-                  },
-                  {
-                    "tag": "button"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><button></button><button></button></body></html>",
-        "noQuirksBodyHtml": "<button></button><button></button>"
-      }
-    },
-    {
-      "data": "<table><tr><td></th>",
-      "errors": [
-        "(1,7): expected-doctype-but-got-start-tag",
-        "(1,20): unexpected-end-tag",
-        "(1,20): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true,
-            "tbody": true,
-            "tr": true,
-            "td": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "tbody",
-                        "children": [
-                          {
-                            "tag": "tr",
-                            "children": [
-                              {
-                                "tag": "td"
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><table><tbody><tr><td></td></tr></tbody></table></body></html>",
-        "noQuirksBodyHtml": "<table><tbody><tr><td></td></tr></tbody></table>"
-      }
-    },
-    {
-      "data": "<table><caption><td>",
-      "errors": [
-        "(1,7): expected-doctype-but-got-start-tag",
-        "(1,20): unexpected-cell-in-table-body",
-        "(1,20): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true,
-            "caption": true,
-            "tbody": true,
-            "tr": true,
-            "td": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "caption"
-                      },
-                      {
-                        "tag": "tbody",
-                        "children": [
-                          {
-                            "tag": "tr",
-                            "children": [
-                              {
-                                "tag": "td"
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><table><caption></caption><tbody><tr><td></td></tr></tbody></table></body></html>",
-        "noQuirksBodyHtml": "<table><caption></caption><tbody><tr><td></td></tr></tbody></table>"
-      }
-    },
-    {
-      "data": "<table><caption><div>",
-      "errors": [
-        "(1,7): expected-doctype-but-got-start-tag",
-        "(1,21): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true,
-            "caption": true,
-            "div": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "caption",
-                        "children": [
-                          {
-                            "tag": "div"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><table><caption><div></div></caption></table></body></html>",
-        "noQuirksBodyHtml": "<table><caption><div></div></caption></table>"
-      }
-    },
-    {
-      "data": "</caption><div>",
-      "errors": [
-        "(1,10): XXX-undefined-error",
-        "(1,15): expected-closing-tag-but-got-eof"
-      ],
-      "fragment": {
-        "name": "caption"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "div": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "div"
-          }
-        ],
-        "html": "<div></div>",
-        "noQuirksBodyHtml": "<div></div>"
-      }
-    },
-    {
-      "data": "<table><caption><div></caption>",
-      "errors": [
-        "(1,7): expected-doctype-but-got-start-tag",
-        "(1,31): expected-one-end-tag-but-got-another",
-        "(1,31): eof-in-table"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true,
-            "caption": true,
-            "div": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "caption",
-                        "children": [
-                          {
-                            "tag": "div"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><table><caption><div></div></caption></table></body></html>",
-        "noQuirksBodyHtml": "<table><caption><div></div></caption></table>"
-      }
-    },
-    {
-      "data": "<table><caption></table>",
-      "errors": [
-        "(1,7): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true,
-            "caption": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "caption"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><table><caption></caption></table></body></html>",
-        "noQuirksBodyHtml": "<table><caption></caption></table>"
-      }
-    },
-    {
-      "data": "</table><div>",
-      "errors": [
-        "(1,8): unexpected-end-tag",
-        "(1,13): expected-closing-tag-but-got-eof"
-      ],
-      "fragment": {
-        "name": "caption"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "div": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "div"
-          }
-        ],
-        "html": "<div></div>",
-        "noQuirksBodyHtml": "<div></div>"
-      }
-    },
-    {
-      "data": "<table><caption></body></col></colgroup></html></tbody></td></tfoot></th></thead></tr>",
-      "errors": [
-        "(1,7): expected-doctype-but-got-start-tag",
-        "(1,23): unexpected-end-tag",
-        "(1,29): unexpected-end-tag",
-        "(1,40): unexpected-end-tag",
-        "(1,47): unexpected-end-tag",
-        "(1,55): unexpected-end-tag",
-        "(1,60): unexpected-end-tag",
-        "(1,68): unexpected-end-tag",
-        "(1,73): unexpected-end-tag",
-        "(1,81): unexpected-end-tag",
-        "(1,86): unexpected-end-tag",
-        "(1,86): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true,
-            "caption": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "caption"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><table><caption></caption></table></body></html>",
-        "noQuirksBodyHtml": "<table><caption></caption></table>"
-      }
-    },
-    {
-      "data": "<table><caption><div></div>",
-      "errors": [
-        "(1,7): expected-doctype-but-got-start-tag",
-        "(1,27): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true,
-            "caption": true,
-            "div": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "caption",
-                        "children": [
-                          {
-                            "tag": "div"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><table><caption><div></div></caption></table></body></html>",
-        "noQuirksBodyHtml": "<table><caption><div></div></caption></table>"
-      }
-    },
-    {
-      "data": "<table><tr><td></body></caption></col></colgroup></html>",
-      "errors": [
-        "(1,7): expected-doctype-but-got-start-tag",
-        "(1,22): unexpected-end-tag",
-        "(1,32): unexpected-end-tag",
-        "(1,38): unexpected-end-tag",
-        "(1,49): unexpected-end-tag",
-        "(1,56): unexpected-end-tag",
-        "(1,56): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true,
-            "tbody": true,
-            "tr": true,
-            "td": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "tbody",
-                        "children": [
-                          {
-                            "tag": "tr",
-                            "children": [
-                              {
-                                "tag": "td"
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><table><tbody><tr><td></td></tr></tbody></table></body></html>",
-        "noQuirksBodyHtml": "<table><tbody><tr><td></td></tr></tbody></table>"
-      }
-    },
-    {
-      "data": "</table></tbody></tfoot></thead></tr><div>",
-      "errors": [
-        "(1,8): unexpected-end-tag",
-        "(1,16): unexpected-end-tag",
-        "(1,24): unexpected-end-tag",
-        "(1,32): unexpected-end-tag",
-        "(1,37): unexpected-end-tag",
-        "(1,42): expected-closing-tag-but-got-eof"
-      ],
-      "fragment": {
-        "name": "td"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "div": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "div"
-          }
-        ],
-        "html": "<div></div>",
-        "noQuirksBodyHtml": "<div></div>"
-      }
-    },
-    {
-      "data": "<table><colgroup>foo",
-      "errors": [
-        "(1,7): expected-doctype-but-got-start-tag",
-        "(1,18): foster-parenting-character-in-table",
-        "(1,19): foster-parenting-character-in-table",
-        "(1,20): foster-parenting-character-in-table",
-        "(1,20): eof-in-table"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true,
-            "colgroup": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "foo"
-                  },
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "colgroup"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>foo<table><colgroup></colgroup></table></body></html>",
-        "noQuirksBodyHtml": "foo<table><colgroup></colgroup></table>"
-      }
-    },
-    {
-      "data": "foo<col>",
-      "errors": [
-        "(1,1): unexpected-character-in-colgroup",
-        "(1,2): unexpected-character-in-colgroup",
-        "(1,3): unexpected-character-in-colgroup"
-      ],
-      "fragment": {
-        "name": "colgroup"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "col": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "col"
-          }
-        ],
-        "html": "<col>",
-        "noQuirksBodyHtml": "foo"
-      }
-    },
-    {
-      "data": "<table><colgroup></col>",
-      "errors": [
-        "(1,7): expected-doctype-but-got-start-tag",
-        "(1,23): no-end-tag",
-        "(1,23): eof-in-table"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true,
-            "colgroup": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "colgroup"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><table><colgroup></colgroup></table></body></html>",
-        "noQuirksBodyHtml": "<table><colgroup></colgroup></table>"
-      }
-    },
-    {
-      "data": "<frameset><div>",
-      "errors": [
-        "(1,10): expected-doctype-but-got-start-tag",
-        "(1,15): unexpected-start-tag-in-frameset",
-        "(1,15): eof-in-frameset"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "frameset": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "frameset"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><frameset></frameset></html>",
-        "noQuirksBodyHtml": "<div></div>"
-      }
-    },
-    {
-      "data": "</frameset><frame>",
-      "errors": [
-        "(1,11): unexpected-frameset-in-frameset-innerhtml"
-      ],
-      "fragment": {
-        "name": "frameset"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "frame": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "frame"
-          }
-        ],
-        "html": "<frame>",
-        "noQuirksBodyHtml": ""
-      }
-    },
-    {
-      "data": "<frameset></div>",
-      "errors": [
-        "(1,10): expected-doctype-but-got-start-tag",
-        "(1,16): unexpected-end-tag-in-frameset",
-        "(1,16): eof-in-frameset"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "frameset": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "frameset"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><frameset></frameset></html>",
-        "noQuirksBodyHtml": ""
-      }
-    },
-    {
-      "data": "</body><div>",
-      "errors": [
-        "(1,7): unexpected-close-tag",
-        "(1,12): expected-closing-tag-but-got-eof"
-      ],
-      "fragment": {
-        "name": "body"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "div": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "div"
-          }
-        ],
-        "html": "<div></div>",
-        "noQuirksBodyHtml": "<div></div>"
-      }
-    },
-    {
-      "data": "<table><tr><div>",
-      "errors": [
-        "(1,7): expected-doctype-but-got-start-tag",
-        "(1,16): unexpected-start-tag-implies-table-voodoo",
-        "(1,16): eof-in-table"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "div": true,
-            "table": true,
-            "tbody": true,
-            "tr": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "div"
-                  },
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "tbody",
-                        "children": [
-                          {
-                            "tag": "tr"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><div></div><table><tbody><tr></tr></tbody></table></body></html>",
-        "noQuirksBodyHtml": "<div></div><table><tbody><tr></tr></tbody></table>"
-      }
-    },
-    {
-      "data": "</tr><td>",
-      "errors": [
-        "(1,5): unexpected-end-tag"
-      ],
-      "fragment": {
-        "name": "tr"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "td": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "td"
-          }
-        ],
-        "html": "<td></td>",
-        "noQuirksBodyHtml": ""
-      }
-    },
-    {
-      "data": "</tbody></tfoot></thead><td>",
-      "errors": [
-        "(1,8): unexpected-end-tag",
-        "(1,16): unexpected-end-tag",
-        "(1,24): unexpected-end-tag"
-      ],
-      "fragment": {
-        "name": "tr"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "td": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "td"
-          }
-        ],
-        "html": "<td></td>",
-        "noQuirksBodyHtml": ""
-      }
-    },
-    {
-      "data": "<table><tr><div><td>",
-      "errors": [
-        "(1,7): expected-doctype-but-got-start-tag",
-        "(1,16): foster-parenting-start-tag",
-        "(1,20): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "div": true,
-            "table": true,
-            "tbody": true,
-            "tr": true,
-            "td": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "div"
-                  },
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "tbody",
-                        "children": [
-                          {
-                            "tag": "tr",
-                            "children": [
-                              {
-                                "tag": "td"
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><div></div><table><tbody><tr><td></td></tr></tbody></table></body></html>",
-        "noQuirksBodyHtml": "<div></div><table><tbody><tr><td></td></tr></tbody></table>"
-      }
-    },
-    {
-      "data": "<caption><col><colgroup><tbody><tfoot><thead><tr>",
-      "errors": [
-        "(1,9): unexpected-start-tag",
-        "(1,14): unexpected-start-tag",
-        "(1,24): unexpected-start-tag",
-        "(1,31): unexpected-start-tag",
-        "(1,38): unexpected-start-tag",
-        "(1,45): unexpected-start-tag"
-      ],
-      "fragment": {
-        "name": "tbody"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "tr": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "tr"
-          }
-        ],
-        "html": "<tr></tr>",
-        "noQuirksBodyHtml": ""
-      }
-    },
-    {
-      "data": "<table><tbody></thead>",
-      "errors": [
-        "(1,7): expected-doctype-but-got-start-tag",
-        "(1,22): unexpected-end-tag-in-table-body",
-        "(1,22): eof-in-table"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true,
-            "tbody": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "tbody"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><table><tbody></tbody></table></body></html>",
-        "noQuirksBodyHtml": "<table><tbody></tbody></table>"
-      }
-    },
-    {
-      "data": "</table><tr>",
-      "errors": [
-        "(1,8): unexpected-end-tag"
-      ],
-      "fragment": {
-        "name": "tbody"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "tr": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "tr"
-          }
-        ],
-        "html": "<tr></tr>",
-        "noQuirksBodyHtml": ""
-      }
-    },
-    {
-      "data": "<table><tbody></body></caption></col></colgroup></html></td></th></tr>",
-      "errors": [
-        "(1,7): expected-doctype-but-got-start-tag",
-        "(1,21): unexpected-end-tag-in-table-body",
-        "(1,31): unexpected-end-tag-in-table-body",
-        "(1,37): unexpected-end-tag-in-table-body",
-        "(1,48): unexpected-end-tag-in-table-body",
-        "(1,55): unexpected-end-tag-in-table-body",
-        "(1,60): unexpected-end-tag-in-table-body",
-        "(1,65): unexpected-end-tag-in-table-body",
-        "(1,70): unexpected-end-tag-in-table-body",
-        "(1,70): eof-in-table"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true,
-            "tbody": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "tbody"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><table><tbody></tbody></table></body></html>",
-        "noQuirksBodyHtml": "<table><tbody></tbody></table>"
-      }
-    },
-    {
-      "data": "<table><tbody></div>",
-      "errors": [
-        "(1,7): expected-doctype-but-got-start-tag",
-        "(1,20): unexpected-end-tag-implies-table-voodoo",
-        "(1,20): end-tag-too-early",
-        "(1,20): eof-in-table"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true,
-            "tbody": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "tbody"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><table><tbody></tbody></table></body></html>",
-        "noQuirksBodyHtml": "<table><tbody></tbody></table>"
-      }
-    },
-    {
-      "data": "<table><table>",
-      "errors": [
-        "(1,7): expected-doctype-but-got-start-tag",
-        "(1,14): unexpected-start-tag-implies-end-tag",
-        "(1,14): eof-in-table"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "table"
-                  },
-                  {
-                    "tag": "table"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><table></table><table></table></body></html>",
-        "noQuirksBodyHtml": "<table></table><table></table>"
-      }
-    },
-    {
-      "data": "<table></body></caption></col></colgroup></html></tbody></td></tfoot></th></thead></tr>",
-      "errors": [
-        "(1,7): expected-doctype-but-got-start-tag",
-        "(1,14): unexpected-end-tag",
-        "(1,24): unexpected-end-tag",
-        "(1,30): unexpected-end-tag",
-        "(1,41): unexpected-end-tag",
-        "(1,48): unexpected-end-tag",
-        "(1,56): unexpected-end-tag",
-        "(1,61): unexpected-end-tag",
-        "(1,69): unexpected-end-tag",
-        "(1,74): unexpected-end-tag",
-        "(1,82): unexpected-end-tag",
-        "(1,87): unexpected-end-tag",
-        "(1,87): eof-in-table"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "table"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><table></table></body></html>",
-        "noQuirksBodyHtml": "<table></table>"
-      }
-    },
-    {
-      "data": "</table><tr>",
-      "errors": [
-        "(1,8): unexpected-end-tag"
-      ],
-      "fragment": {
-        "name": "table"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "tbody": true,
-            "tr": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "tbody",
-            "children": [
-              {
-                "tag": "tr"
-              }
-            ]
-          }
-        ],
-        "html": "<tbody><tr></tr></tbody>",
-        "noQuirksBodyHtml": ""
-      }
-    },
-    {
-      "data": "<body></body></html>",
-      "errors": [
-        "(1,20): unexpected-end-tag-after-body-innerhtml"
-      ],
-      "fragment": {
-        "name": "html"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "head": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "head"
-          },
-          {
-            "tag": "body"
-          }
-        ],
-        "html": "<head></head><body></body>",
-        "noQuirksBodyHtml": ""
-      }
-    },
-    {
-      "data": "<html><frameset></frameset></html> ",
-      "errors": [
-        "(1,6): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "frameset": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "frameset"
-              },
-              {
-                "text": " "
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><frameset></frameset> </html>",
-        "noQuirksBodyHtml": " "
-      }
-    },
-    {
-      "data": "<!DOCTYPE html PUBLIC \"-//W3C//DTD HTML 4.01//EN\"><html></html>",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html \"-//W3C//DTD HTML 4.01//EN\" \"\""
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body></body></html>",
-        "noQuirksBodyHtml": ""
-      }
-    },
-    {
-      "data": "<param><frameset></frameset>",
-      "errors": [
-        "(1,7): expected-doctype-but-got-start-tag",
-        "(1,17): unexpected-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "frameset": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "frameset"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><frameset></frameset></html>",
-        "noQuirksBodyHtml": "<param>"
-      }
-    },
-    {
-      "data": "<source><frameset></frameset>",
-      "errors": [
-        "(1,8): expected-doctype-but-got-start-tag",
-        "(1,18): unexpected-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "frameset": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "frameset"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><frameset></frameset></html>",
-        "noQuirksBodyHtml": "<source>"
-      }
-    },
-    {
-      "data": "<track><frameset></frameset>",
-      "errors": [
-        "(1,7): expected-doctype-but-got-start-tag",
-        "(1,17): unexpected-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "frameset": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "frameset"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><frameset></frameset></html>",
-        "noQuirksBodyHtml": "<track>"
-      }
-    },
-    {
-      "data": "</html><frameset></frameset>",
-      "errors": [
-        "(1,7): expected-doctype-but-got-end-tag",
-        "(1,17): expected-eof-but-got-start-tag",
-        "(1,17): unexpected-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "frameset": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "frameset"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><frameset></frameset></html>",
-        "noQuirksBodyHtml": ""
-      }
-    },
-    {
-      "data": "</body><frameset></frameset>",
-      "errors": [
-        "(1,7): expected-doctype-but-got-end-tag",
-        "(1,17): unexpected-start-tag-after-body",
-        "(1,17): unexpected-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "frameset": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "frameset"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><frameset></frameset></html>",
-        "noQuirksBodyHtml": ""
-      }
-    }
-  ],
-  "tests7.dat": [
-    {
-      "data": "<!doctype html><body><title>X</title>",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "title": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "title",
-                    "children": [
-                      {
-                        "text": "X"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><title>X</title></body></html>",
-        "noQuirksBodyHtml": "<title>X</title>"
-      }
-    },
-    {
-      "data": "<!doctype html><table><title>X</title></table>",
-      "errors": [
-        "(1,29): unexpected-start-tag-implies-table-voodoo"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "title": true,
-            "table": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "title",
-                    "children": [
-                      {
-                        "text": "X"
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "table"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><title>X</title><table></table></body></html>",
-        "noQuirksBodyHtml": "<title>X</title><table></table>"
-      }
-    },
-    {
-      "data": "<!doctype html><head></head><title>X</title>",
-      "errors": [
-        "(1,35): unexpected-start-tag-out-of-my-head"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "title": true,
-            "body": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "title",
-                    "children": [
-                      {
-                        "text": "X"
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head><title>X</title></head><body></body></html>",
-        "noQuirksBodyHtml": "<title>X</title>"
-      }
-    },
-    {
-      "data": "<!doctype html></head><title>X</title>",
-      "errors": [
-        "(1,29): unexpected-start-tag-out-of-my-head"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "title": true,
-            "body": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "title",
-                    "children": [
-                      {
-                        "text": "X"
-                      }
-                    ]
-                  }
-                ]
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head><title>X</title></head><body></body></html>",
-        "noQuirksBodyHtml": "<title>X</title>"
-      }
-    },
-    {
-      "data": "<!doctype html><table><meta></table>",
-      "errors": [
-        "(1,28): unexpected-start-tag-implies-table-voodoo"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "meta": true,
-            "table": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "meta"
-                  },
-                  {
-                    "tag": "table"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><meta><table></table></body></html>",
-        "noQuirksBodyHtml": "<meta><table></table>"
-      }
-    },
-    {
-      "data": "<!doctype html><table>X<tr><td><table> <meta></table></table>",
-      "errors": [
-        "unexpected text in table",
-        "(1,45): unexpected-start-tag-implies-table-voodoo"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true,
-            "tbody": true,
-            "tr": true,
-            "td": true,
-            "meta": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "X"
-                  },
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "tbody",
-                        "children": [
-                          {
-                            "tag": "tr",
-                            "children": [
-                              {
-                                "tag": "td",
-                                "children": [
-                                  {
-                                    "tag": "meta"
-                                  },
-                                  {
-                                    "tag": "table",
-                                    "children": [
-                                      {
-                                        "text": " "
-                                      }
-                                    ]
-                                  }
-                                ]
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body>X<table><tbody><tr><td><meta><table> </table></td></tr></tbody></table></body></html>",
-        "noQuirksBodyHtml": "X<table><tbody><tr><td><meta><table> </table></td></tr></tbody></table>"
-      }
-    },
-    {
-      "data": "<!doctype html><html> <head>",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body></body></html>",
-        "noQuirksBodyHtml": " "
-      }
-    },
-    {
-      "data": "<!doctype html> <head>",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body></body></html>",
-        "noQuirksBodyHtml": " "
-      }
-    },
-    {
-      "data": "<!doctype html><table><style> <tr>x </style> </table>",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true,
-            "style": true
-          },
-          "doctype": true,
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "style",
-                        "children": [
-                          {
-                            "text": " <tr>x ",
-                            "no_escape": true
-                          }
-                        ]
-                      },
-                      {
-                        "text": " "
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><table><style> <tr>x </style> </table></body></html>",
-        "noQuirksBodyHtml": "<table><style> <tr>x </style> </table>"
-      }
-    },
-    {
-      "data": "<!doctype html><table><TBODY><script> <tr>x </script> </table>",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true,
-            "tbody": true,
-            "script": true
-          },
-          "doctype": true,
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "tbody",
-                        "children": [
-                          {
-                            "tag": "script",
-                            "children": [
-                              {
-                                "text": " <tr>x ",
-                                "no_escape": true
-                              }
-                            ]
-                          },
-                          {
-                            "text": " "
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><table><tbody><script> <tr>x </script> </tbody></table></body></html>",
-        "noQuirksBodyHtml": "<table><tbody><script> <tr>x </script> </tbody></table>"
-      }
-    },
-    {
-      "data": "<!doctype html><p><applet><p>X</p></applet>",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "p": true,
-            "applet": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "p",
-                    "children": [
-                      {
-                        "tag": "applet",
-                        "children": [
-                          {
-                            "tag": "p",
-                            "children": [
-                              {
-                                "text": "X"
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><p><applet><p>X</p></applet></p></body></html>",
-        "noQuirksBodyHtml": "<p><applet><p>X</p></applet></p>"
-      }
-    },
-    {
-      "data": "<!doctype html><p><object type=\"application/x-non-existant-plugin\"><p>X</p></object>",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "p": true,
-            "object": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "p",
-                    "children": [
-                      {
-                        "tag": "object",
-                        "attrs": [
-                          {
-                            "name": "type",
-                            "value": "application/x-non-existant-plugin"
-                          }
-                        ],
-                        "children": [
-                          {
-                            "tag": "p",
-                            "children": [
-                              {
-                                "text": "X"
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><p><object type=\"application/x-non-existant-plugin\"><p>X</p></object></p></body></html>",
-        "noQuirksBodyHtml": "<p><object type=\"application/x-non-existant-plugin\"><p>X</p></object></p>"
-      }
-    },
-    {
-      "data": "<!doctype html><listing>\nX</listing>",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "listing": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "listing",
-                    "children": [
-                      {
-                        "text": "X"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><listing>X</listing></body></html>",
-        "noQuirksBodyHtml": "<listing>X</listing>"
-      }
-    },
-    {
-      "data": "<!doctype html><select><input>X",
-      "errors": [
-        "(1,30): unexpected-input-in-select"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "select": true,
-            "input": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "select"
-                  },
-                  {
-                    "tag": "input"
-                  },
-                  {
-                    "text": "X"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><select></select><input>X</body></html>",
-        "noQuirksBodyHtml": "<select></select><input>X"
-      }
-    },
-    {
-      "data": "<!doctype html><select><select>X",
-      "errors": [
-        "(1,31): unexpected-select-in-select"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "select": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "select"
-                  },
-                  {
-                    "text": "X"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><select></select>X</body></html>",
-        "noQuirksBodyHtml": "<select></select>X"
-      }
-    },
-    {
-      "data": "<!doctype html><table><input type=hidDEN></table>",
-      "errors": [
-        "(1,41): unexpected-hidden-input-in-table"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true,
-            "input": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "input",
-                        "attrs": [
-                          {
-                            "name": "type",
-                            "value": "hidDEN"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><table><input type=\"hidDEN\"></table></body></html>",
-        "noQuirksBodyHtml": "<table><input type=\"hidDEN\"></table>"
-      }
-    },
-    {
-      "data": "<!doctype html><table>X<input type=hidDEN></table>",
-      "errors": [
-        "(1,23): foster-parenting-character",
-        "(1,42): unexpected-hidden-input-in-table"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true,
-            "input": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "X"
-                  },
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "input",
-                        "attrs": [
-                          {
-                            "name": "type",
-                            "value": "hidDEN"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body>X<table><input type=\"hidDEN\"></table></body></html>",
-        "noQuirksBodyHtml": "X<table><input type=\"hidDEN\"></table>"
-      }
-    },
-    {
-      "data": "<!doctype html><table>  <input type=hidDEN></table>",
-      "errors": [
-        "(1,43): unexpected-hidden-input-in-table"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true,
-            "input": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "text": "  "
-                      },
-                      {
-                        "tag": "input",
-                        "attrs": [
-                          {
-                            "name": "type",
-                            "value": "hidDEN"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><table>  <input type=\"hidDEN\"></table></body></html>",
-        "noQuirksBodyHtml": "<table>  <input type=\"hidDEN\"></table>"
-      }
-    },
-    {
-      "data": "<!doctype html><table>  <input type='hidDEN'></table>",
-      "errors": [
-        "(1,45): unexpected-hidden-input-in-table"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true,
-            "input": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "text": "  "
-                      },
-                      {
-                        "tag": "input",
-                        "attrs": [
-                          {
-                            "name": "type",
-                            "value": "hidDEN"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><table>  <input type=\"hidDEN\"></table></body></html>",
-        "noQuirksBodyHtml": "<table>  <input type=\"hidDEN\"></table>"
-      }
-    },
-    {
-      "data": "<!doctype html><table><input type=\" hidden\"><input type=hidDEN></table>",
-      "errors": [
-        "(1,44): unexpected-start-tag-implies-table-voodoo",
-        "(1,63): unexpected-hidden-input-in-table"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "input": true,
-            "table": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "input",
-                    "attrs": [
-                      {
-                        "name": "type",
-                        "value": " hidden"
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "input",
-                        "attrs": [
-                          {
-                            "name": "type",
-                            "value": "hidDEN"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><input type=\" hidden\"><table><input type=\"hidDEN\"></table></body></html>",
-        "noQuirksBodyHtml": "<input type=\" hidden\"><table><input type=\"hidDEN\"></table>"
-      }
-    },
-    {
-      "data": "<!doctype html><table><select>X<tr>",
-      "errors": [
-        "(1,30): unexpected-start-tag-implies-table-voodoo",
-        "(1,35): unexpected-table-element-start-tag-in-select-in-table",
-        "(1,35): eof-in-table"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "select": true,
-            "table": true,
-            "tbody": true,
-            "tr": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "select",
-                    "children": [
-                      {
-                        "text": "X"
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "tbody",
-                        "children": [
-                          {
-                            "tag": "tr"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><select>X</select><table><tbody><tr></tr></tbody></table></body></html>",
-        "noQuirksBodyHtml": "<select>X</select><table><tbody><tr></tr></tbody></table>"
-      }
-    },
-    {
-      "data": "<!doctype html><select>X</select>",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "select": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "select",
-                    "children": [
-                      {
-                        "text": "X"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><select>X</select></body></html>",
-        "noQuirksBodyHtml": "<select>X</select>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE hTmL><html></html>",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body></body></html>",
-        "noQuirksBodyHtml": ""
-      }
-    },
-    {
-      "data": "<!DOCTYPE HTML><html></html>",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body></body></html>",
-        "noQuirksBodyHtml": ""
-      }
-    },
-    {
-      "data": "<body>X</body></body>",
-      "errors": [
-        "(1,21): unexpected-end-tag-after-body"
-      ],
-      "fragment": {
-        "name": "html"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "head": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "head"
-          },
-          {
-            "tag": "body",
-            "children": [
-              {
-                "text": "X"
-              }
-            ]
-          }
-        ],
-        "html": "<head></head><body>X</body>",
-        "noQuirksBodyHtml": "X"
-      }
-    },
-    {
-      "data": "<div><p>a</x> b",
-      "errors": [
-        "(1,5): expected-doctype-but-got-start-tag",
-        "(1,13): unexpected-end-tag",
-        "(1,15): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "div": true,
-            "p": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "div",
-                    "children": [
-                      {
-                        "tag": "p",
-                        "children": [
-                          {
-                            "text": "a b"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><div><p>a b</p></div></body></html>",
-        "noQuirksBodyHtml": "<div><p>a b</p></div>"
-      }
-    },
-    {
-      "data": "<table><tr><td><code></code> </table>",
-      "errors": [
-        "(1,7): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true,
-            "tbody": true,
-            "tr": true,
-            "td": true,
-            "code": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "tbody",
-                        "children": [
-                          {
-                            "tag": "tr",
-                            "children": [
-                              {
-                                "tag": "td",
-                                "children": [
-                                  {
-                                    "tag": "code"
-                                  },
-                                  {
-                                    "text": " "
-                                  }
-                                ]
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><table><tbody><tr><td><code></code> </td></tr></tbody></table></body></html>",
-        "noQuirksBodyHtml": "<table><tbody><tr><td><code></code> </td></tr></tbody></table>"
-      }
-    },
-    {
-      "data": "<table><b><tr><td>aaa</td></tr>bbb</table>ccc",
-      "errors": [
-        "(1,7): expected-doctype-but-got-start-tag",
-        "(1,10): foster-parenting-start-tag",
-        "(1,32): foster-parenting-character",
-        "(1,33): foster-parenting-character",
-        "(1,34): foster-parenting-character",
-        "(1,45): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "b": true,
-            "table": true,
-            "tbody": true,
-            "tr": true,
-            "td": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "b"
-                  },
-                  {
-                    "tag": "b",
-                    "children": [
-                      {
-                        "text": "bbb"
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "tbody",
-                        "children": [
-                          {
-                            "tag": "tr",
-                            "children": [
-                              {
-                                "tag": "td",
-                                "children": [
-                                  {
-                                    "text": "aaa"
-                                  }
-                                ]
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "b",
-                    "children": [
-                      {
-                        "text": "ccc"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><b></b><b>bbb</b><table><tbody><tr><td>aaa</td></tr></tbody></table><b>ccc</b></body></html>",
-        "noQuirksBodyHtml": "<b></b><b>bbb</b><table><tbody><tr><td>aaa</td></tr></tbody></table><b>ccc</b>"
-      }
-    },
-    {
-      "data": "A<table><tr> B</tr> B</table>",
-      "errors": [
-        "(1,1): expected-doctype-but-got-chars",
-        "(1,13): foster-parenting-character",
-        "(1,14): foster-parenting-character",
-        "(1,20): foster-parenting-character",
-        "(1,21): foster-parenting-character"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true,
-            "tbody": true,
-            "tr": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "A B B"
-                  },
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "tbody",
-                        "children": [
-                          {
-                            "tag": "tr"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>A B B<table><tbody><tr></tr></tbody></table></body></html>",
-        "noQuirksBodyHtml": "A B B<table><tbody><tr></tr></tbody></table>"
-      }
-    },
-    {
-      "data": "A<table><tr> B</tr> </em>C</table>",
-      "errors": [
-        "(1,1): expected-doctype-but-got-chars",
-        "(1,13): foster-parenting-character",
-        "(1,14): foster-parenting-character",
-        "(1,20): foster-parenting-character",
-        "(1,25): unexpected-end-tag",
-        "(1,25): unexpected-end-tag-in-special-element",
-        "(1,26): foster-parenting-character"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true,
-            "tbody": true,
-            "tr": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "A BC"
-                  },
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "tbody",
-                        "children": [
-                          {
-                            "tag": "tr"
-                          },
-                          {
-                            "text": " "
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>A BC<table><tbody><tr></tr> </tbody></table></body></html>",
-        "noQuirksBodyHtml": "A BC<table><tbody><tr></tr> </tbody></table>"
-      }
-    },
-    {
-      "data": "<select><keygen>",
-      "errors": [
-        "(1,8): expected-doctype-but-got-start-tag",
-        "(1,16): unexpected-input-in-select"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "select": true,
-            "keygen": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "select"
-                  },
-                  {
-                    "tag": "keygen"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><select></select><keygen></body></html>",
-        "noQuirksBodyHtml": "<select></select><keygen>"
-      }
-    }
-  ],
-  "tests8.dat": [
-    {
-      "data": "<div>\n<div></div>\n</span>x",
-      "errors": [
-        "(1,5): expected-doctype-but-got-start-tag",
-        "(3,7): unexpected-end-tag",
-        "(3,8): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "div": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "div",
-                    "children": [
-                      {
-                        "text": "\n"
-                      },
-                      {
-                        "tag": "div"
-                      },
-                      {
-                        "text": "\nx"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><div>\n<div></div>\nx</div></body></html>",
-        "noQuirksBodyHtml": "<div>\n<div></div>\nx</div>"
-      }
-    },
-    {
-      "data": "<div>x<div></div>\n</span>x",
-      "errors": [
-        "(1,5): expected-doctype-but-got-start-tag",
-        "(2,7): unexpected-end-tag",
-        "(2,8): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "div": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "div",
-                    "children": [
-                      {
-                        "text": "x"
-                      },
-                      {
-                        "tag": "div"
-                      },
-                      {
-                        "text": "\nx"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><div>x<div></div>\nx</div></body></html>",
-        "noQuirksBodyHtml": "<div>x<div></div>\nx</div>"
-      }
-    },
-    {
-      "data": "<div>x<div></div>x</span>x",
-      "errors": [
-        "(1,5): expected-doctype-but-got-start-tag",
-        "(1,25): unexpected-end-tag",
-        "(1,26): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "div": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "div",
-                    "children": [
-                      {
-                        "text": "x"
-                      },
-                      {
-                        "tag": "div"
-                      },
-                      {
-                        "text": "xx"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><div>x<div></div>xx</div></body></html>",
-        "noQuirksBodyHtml": "<div>x<div></div>xx</div>"
-      }
-    },
-    {
-      "data": "<div>x<div></div>y</span>z",
-      "errors": [
-        "(1,5): expected-doctype-but-got-start-tag",
-        "(1,25): unexpected-end-tag",
-        "(1,26): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "div": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "div",
-                    "children": [
-                      {
-                        "text": "x"
-                      },
-                      {
-                        "tag": "div"
-                      },
-                      {
-                        "text": "yz"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><div>x<div></div>yz</div></body></html>",
-        "noQuirksBodyHtml": "<div>x<div></div>yz</div>"
-      }
-    },
-    {
-      "data": "<table><div>x<div></div>x</span>x",
-      "errors": [
-        "(1,7): expected-doctype-but-got-start-tag",
-        "(1,12): foster-parenting-start-tag",
-        "(1,13): foster-parenting-character",
-        "(1,18): foster-parenting-start-tag",
-        "(1,24): foster-parenting-end-tag",
-        "(1,25): foster-parenting-start-tag",
-        "(1,32): foster-parenting-end-tag",
-        "(1,32): unexpected-end-tag",
-        "(1,33): foster-parenting-character",
-        "(1,33): eof-in-table"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "div": true,
-            "table": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "div",
-                    "children": [
-                      {
-                        "text": "x"
-                      },
-                      {
-                        "tag": "div"
-                      },
-                      {
-                        "text": "xx"
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "table"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><div>x<div></div>xx</div><table></table></body></html>",
-        "noQuirksBodyHtml": "<div>x<div></div>xx</div><table></table>"
-      }
-    },
-    {
-      "data": "x<table>x",
-      "errors": [
-        "(1,1): expected-doctype-but-got-chars",
-        "(1,9): foster-parenting-character",
-        "(1,9): eof-in-table"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "xx"
-                  },
-                  {
-                    "tag": "table"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>xx<table></table></body></html>",
-        "noQuirksBodyHtml": "xx<table></table>"
-      }
-    },
-    {
-      "data": "x<table><table>x",
-      "errors": [
-        "(1,1): expected-doctype-but-got-chars",
-        "(1,15): unexpected-start-tag-implies-end-tag",
-        "(1,16): foster-parenting-character",
-        "(1,16): eof-in-table"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "x"
-                  },
-                  {
-                    "tag": "table"
-                  },
-                  {
-                    "text": "x"
-                  },
-                  {
-                    "tag": "table"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>x<table></table>x<table></table></body></html>",
-        "noQuirksBodyHtml": "x<table></table>x<table></table>"
-      }
-    },
-    {
-      "data": "<b>a<div></div><div></b>y",
-      "errors": [
-        "(1,3): expected-doctype-but-got-start-tag",
-        "(1,24): adoption-agency-1.3",
-        "(1,25): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "b": true,
-            "div": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "b",
-                    "children": [
-                      {
-                        "text": "a"
-                      },
-                      {
-                        "tag": "div"
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "div",
-                    "children": [
-                      {
-                        "tag": "b"
-                      },
-                      {
-                        "text": "y"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><b>a<div></div></b><div><b></b>y</div></body></html>",
-        "noQuirksBodyHtml": "<b>a<div></div></b><div><b></b>y</div>"
-      }
-    },
-    {
-      "data": "<a><div><p></a>",
-      "errors": [
-        "(1,3): expected-doctype-but-got-start-tag",
-        "(1,15): adoption-agency-1.3",
-        "(1,15): adoption-agency-1.3",
-        "(1,15): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "a": true,
-            "div": true,
-            "p": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "a"
-                  },
-                  {
-                    "tag": "div",
-                    "children": [
-                      {
-                        "tag": "a"
-                      },
-                      {
-                        "tag": "p",
-                        "children": [
-                          {
-                            "tag": "a"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><a></a><div><a></a><p><a></a></p></div></body></html>",
-        "noQuirksBodyHtml": "<a></a><div><a></a><p><a></a></p></div>"
-      }
-    }
-  ],
-  "tests9.dat": [
-    {
-      "data": "<!DOCTYPE html><math></math>",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "math math": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "math",
-                    "ns": "http://www.w3.org/1998/Math/MathML"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><math></math></body></html>",
-        "noQuirksBodyHtml": "<math></math>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><body><math></math>",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "math math": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "math",
-                    "ns": "http://www.w3.org/1998/Math/MathML"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><math></math></body></html>",
-        "noQuirksBodyHtml": "<math></math>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><math><mi>",
-      "errors": [
-        "(1,25) expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "math math": true,
-            "math mi": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "math",
-                    "ns": "http://www.w3.org/1998/Math/MathML",
-                    "children": [
-                      {
-                        "tag": "mi",
-                        "ns": "http://www.w3.org/1998/Math/MathML"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><math><mi></mi></math></body></html>",
-        "noQuirksBodyHtml": "<math><mi></mi></math>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><math><annotation-xml><svg><u>",
-      "errors": [
-        "(1,45) unexpected-html-element-in-foreign-content",
-        "(1,45) expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "math math": true,
-            "math annotation-xml": true,
-            "svg svg": true,
-            "u": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "math",
-                    "ns": "http://www.w3.org/1998/Math/MathML",
-                    "children": [
-                      {
-                        "tag": "annotation-xml",
-                        "ns": "http://www.w3.org/1998/Math/MathML",
-                        "children": [
-                          {
-                            "tag": "svg",
-                            "ns": "http://www.w3.org/2000/svg"
-                          }
-                        ]
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "u"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><math><annotation-xml><svg></svg></annotation-xml></math><u></u></body></html>",
-        "noQuirksBodyHtml": "<math><annotation-xml><svg><u></u></svg></annotation-xml></math>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><body><select><math></math></select>",
-      "errors": [
-        "(1,35) unexpected-start-tag-in-select",
-        "(1,42) unexpected-end-tag-in-select"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "select": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "select"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><select></select></body></html>",
-        "noQuirksBodyHtml": "<select></select>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><body><select><option><math></math></option></select>",
-      "errors": [
-        "(1,43) unexpected-start-tag-in-select",
-        "(1,50) unexpected-end-tag-in-select"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "select": true,
-            "option": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "select",
-                    "children": [
-                      {
-                        "tag": "option"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><select><option></option></select></body></html>",
-        "noQuirksBodyHtml": "<select><option></option></select>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><body><table><math></math></table>",
-      "errors": [
-        "(1,34) unexpected-start-tag-implies-table-voodoo"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "math math": true,
-            "table": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "math",
-                    "ns": "http://www.w3.org/1998/Math/MathML"
-                  },
-                  {
-                    "tag": "table"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><math></math><table></table></body></html>",
-        "noQuirksBodyHtml": "<math></math><table></table>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><body><table><math><mi>foo</mi></math></table>",
-      "errors": [
-        "(1,34) foster-parenting-start-token",
-        "(1,39) foster-parenting-character",
-        "(1,40) foster-parenting-character",
-        "(1,41) foster-parenting-character"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "math math": true,
-            "math mi": true,
-            "table": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "math",
-                    "ns": "http://www.w3.org/1998/Math/MathML",
-                    "children": [
-                      {
-                        "tag": "mi",
-                        "ns": "http://www.w3.org/1998/Math/MathML",
-                        "children": [
-                          {
-                            "text": "foo"
-                          }
-                        ]
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "table"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><math><mi>foo</mi></math><table></table></body></html>",
-        "noQuirksBodyHtml": "<math><mi>foo</mi></math><table></table>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><body><table><math><mi>foo</mi><mi>bar</mi></math></table>",
-      "errors": [
-        "(1,34) foster-parenting-start-tag",
-        "(1,39) foster-parenting-character",
-        "(1,40) foster-parenting-character",
-        "(1,41) foster-parenting-character",
-        "(1,51) foster-parenting-character",
-        "(1,52) foster-parenting-character",
-        "(1,53) foster-parenting-character"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "math math": true,
-            "math mi": true,
-            "table": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "math",
-                    "ns": "http://www.w3.org/1998/Math/MathML",
-                    "children": [
-                      {
-                        "tag": "mi",
-                        "ns": "http://www.w3.org/1998/Math/MathML",
-                        "children": [
-                          {
-                            "text": "foo"
-                          }
-                        ]
-                      },
-                      {
-                        "tag": "mi",
-                        "ns": "http://www.w3.org/1998/Math/MathML",
-                        "children": [
-                          {
-                            "text": "bar"
-                          }
-                        ]
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "table"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><math><mi>foo</mi><mi>bar</mi></math><table></table></body></html>",
-        "noQuirksBodyHtml": "<math><mi>foo</mi><mi>bar</mi></math><table></table>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><body><table><tbody><math><mi>foo</mi><mi>bar</mi></math></tbody></table>",
-      "errors": [
-        "(1,41) foster-parenting-start-tag",
-        "(1,46) foster-parenting-character",
-        "(1,47) foster-parenting-character",
-        "(1,48) foster-parenting-character",
-        "(1,58) foster-parenting-character",
-        "(1,59) foster-parenting-character",
-        "(1,60) foster-parenting-character"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "math math": true,
-            "math mi": true,
-            "table": true,
-            "tbody": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "math",
-                    "ns": "http://www.w3.org/1998/Math/MathML",
-                    "children": [
-                      {
-                        "tag": "mi",
-                        "ns": "http://www.w3.org/1998/Math/MathML",
-                        "children": [
-                          {
-                            "text": "foo"
-                          }
-                        ]
-                      },
-                      {
-                        "tag": "mi",
-                        "ns": "http://www.w3.org/1998/Math/MathML",
-                        "children": [
-                          {
-                            "text": "bar"
-                          }
-                        ]
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "tbody"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><math><mi>foo</mi><mi>bar</mi></math><table><tbody></tbody></table></body></html>",
-        "noQuirksBodyHtml": "<math><mi>foo</mi><mi>bar</mi></math><table><tbody></tbody></table>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><body><table><tbody><tr><math><mi>foo</mi><mi>bar</mi></math></tr></tbody></table>",
-      "errors": [
-        "(1,45) foster-parenting-start-tag",
-        "(1,50) foster-parenting-character",
-        "(1,51) foster-parenting-character",
-        "(1,52) foster-parenting-character",
-        "(1,62) foster-parenting-character",
-        "(1,63) foster-parenting-character",
-        "(1,64) foster-parenting-character"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "math math": true,
-            "math mi": true,
-            "table": true,
-            "tbody": true,
-            "tr": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "math",
-                    "ns": "http://www.w3.org/1998/Math/MathML",
-                    "children": [
-                      {
-                        "tag": "mi",
-                        "ns": "http://www.w3.org/1998/Math/MathML",
-                        "children": [
-                          {
-                            "text": "foo"
-                          }
-                        ]
-                      },
-                      {
-                        "tag": "mi",
-                        "ns": "http://www.w3.org/1998/Math/MathML",
-                        "children": [
-                          {
-                            "text": "bar"
-                          }
-                        ]
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "tbody",
-                        "children": [
-                          {
-                            "tag": "tr"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><math><mi>foo</mi><mi>bar</mi></math><table><tbody><tr></tr></tbody></table></body></html>",
-        "noQuirksBodyHtml": "<math><mi>foo</mi><mi>bar</mi></math><table><tbody><tr></tr></tbody></table>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><body><table><tbody><tr><td><math><mi>foo</mi><mi>bar</mi></math></td></tr></tbody></table>",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true,
-            "tbody": true,
-            "tr": true,
-            "td": true,
-            "math math": true,
-            "math mi": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "tbody",
-                        "children": [
-                          {
-                            "tag": "tr",
-                            "children": [
-                              {
-                                "tag": "td",
-                                "children": [
-                                  {
-                                    "tag": "math",
-                                    "ns": "http://www.w3.org/1998/Math/MathML",
-                                    "children": [
-                                      {
-                                        "tag": "mi",
-                                        "ns": "http://www.w3.org/1998/Math/MathML",
-                                        "children": [
-                                          {
-                                            "text": "foo"
-                                          }
-                                        ]
-                                      },
-                                      {
-                                        "tag": "mi",
-                                        "ns": "http://www.w3.org/1998/Math/MathML",
-                                        "children": [
-                                          {
-                                            "text": "bar"
-                                          }
-                                        ]
-                                      }
-                                    ]
-                                  }
-                                ]
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><table><tbody><tr><td><math><mi>foo</mi><mi>bar</mi></math></td></tr></tbody></table></body></html>",
-        "noQuirksBodyHtml": "<table><tbody><tr><td><math><mi>foo</mi><mi>bar</mi></math></td></tr></tbody></table>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><body><table><tbody><tr><td><math><mi>foo</mi><mi>bar</mi></math><p>baz</td></tr></tbody></table>",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true,
-            "tbody": true,
-            "tr": true,
-            "td": true,
-            "math math": true,
-            "math mi": true,
-            "p": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "tbody",
-                        "children": [
-                          {
-                            "tag": "tr",
-                            "children": [
-                              {
-                                "tag": "td",
-                                "children": [
-                                  {
-                                    "tag": "math",
-                                    "ns": "http://www.w3.org/1998/Math/MathML",
-                                    "children": [
-                                      {
-                                        "tag": "mi",
-                                        "ns": "http://www.w3.org/1998/Math/MathML",
-                                        "children": [
-                                          {
-                                            "text": "foo"
-                                          }
-                                        ]
-                                      },
-                                      {
-                                        "tag": "mi",
-                                        "ns": "http://www.w3.org/1998/Math/MathML",
-                                        "children": [
-                                          {
-                                            "text": "bar"
-                                          }
-                                        ]
-                                      }
-                                    ]
-                                  },
-                                  {
-                                    "tag": "p",
-                                    "children": [
-                                      {
-                                        "text": "baz"
-                                      }
-                                    ]
-                                  }
-                                ]
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><table><tbody><tr><td><math><mi>foo</mi><mi>bar</mi></math><p>baz</p></td></tr></tbody></table></body></html>",
-        "noQuirksBodyHtml": "<table><tbody><tr><td><math><mi>foo</mi><mi>bar</mi></math><p>baz</p></td></tr></tbody></table>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><body><table><caption><math><mi>foo</mi><mi>bar</mi></math><p>baz</caption></table>",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true,
-            "caption": true,
-            "math math": true,
-            "math mi": true,
-            "p": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "caption",
-                        "children": [
-                          {
-                            "tag": "math",
-                            "ns": "http://www.w3.org/1998/Math/MathML",
-                            "children": [
-                              {
-                                "tag": "mi",
-                                "ns": "http://www.w3.org/1998/Math/MathML",
-                                "children": [
-                                  {
-                                    "text": "foo"
-                                  }
-                                ]
-                              },
-                              {
-                                "tag": "mi",
-                                "ns": "http://www.w3.org/1998/Math/MathML",
-                                "children": [
-                                  {
-                                    "text": "bar"
-                                  }
-                                ]
-                              }
-                            ]
-                          },
-                          {
-                            "tag": "p",
-                            "children": [
-                              {
-                                "text": "baz"
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><table><caption><math><mi>foo</mi><mi>bar</mi></math><p>baz</p></caption></table></body></html>",
-        "noQuirksBodyHtml": "<table><caption><math><mi>foo</mi><mi>bar</mi></math><p>baz</p></caption></table>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><body><table><caption><math><mi>foo</mi><mi>bar</mi><p>baz</table><p>quux",
-      "errors": [
-        "(1,70) unexpected-html-element-in-foreign-content"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true,
-            "caption": true,
-            "math math": true,
-            "math mi": true,
-            "p": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "caption",
-                        "children": [
-                          {
-                            "tag": "math",
-                            "ns": "http://www.w3.org/1998/Math/MathML",
-                            "children": [
-                              {
-                                "tag": "mi",
-                                "ns": "http://www.w3.org/1998/Math/MathML",
-                                "children": [
-                                  {
-                                    "text": "foo"
-                                  }
-                                ]
-                              },
-                              {
-                                "tag": "mi",
-                                "ns": "http://www.w3.org/1998/Math/MathML",
-                                "children": [
-                                  {
-                                    "text": "bar"
-                                  }
-                                ]
-                              }
-                            ]
-                          },
-                          {
-                            "tag": "p",
-                            "children": [
-                              {
-                                "text": "baz"
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "p",
-                    "children": [
-                      {
-                        "text": "quux"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><table><caption><math><mi>foo</mi><mi>bar</mi></math><p>baz</p></caption></table><p>quux</p></body></html>",
-        "noQuirksBodyHtml": "<table><caption><math><mi>foo</mi><mi>bar</mi><p>baz</p></math></caption></table><p>quux</p>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><body><table><caption><math><mi>foo</mi><mi>bar</mi>baz</table><p>quux",
-      "errors": [
-        "(1,78) unexpected-end-tag",
-        "(1,78) expected-one-end-tag-but-got-another"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true,
-            "caption": true,
-            "math math": true,
-            "math mi": true,
-            "p": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "caption",
-                        "children": [
-                          {
-                            "tag": "math",
-                            "ns": "http://www.w3.org/1998/Math/MathML",
-                            "children": [
-                              {
-                                "tag": "mi",
-                                "ns": "http://www.w3.org/1998/Math/MathML",
-                                "children": [
-                                  {
-                                    "text": "foo"
-                                  }
-                                ]
-                              },
-                              {
-                                "tag": "mi",
-                                "ns": "http://www.w3.org/1998/Math/MathML",
-                                "children": [
-                                  {
-                                    "text": "bar"
-                                  }
-                                ]
-                              },
-                              {
-                                "text": "baz"
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "p",
-                    "children": [
-                      {
-                        "text": "quux"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><table><caption><math><mi>foo</mi><mi>bar</mi>baz</math></caption></table><p>quux</p></body></html>",
-        "noQuirksBodyHtml": "<table><caption><math><mi>foo</mi><mi>bar</mi>baz</math></caption></table><p>quux</p>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><body><table><colgroup><math><mi>foo</mi><mi>bar</mi><p>baz</table><p>quux",
-      "errors": [
-        "(1,44) foster-parenting-start-tag",
-        "(1,49) foster-parenting-character",
-        "(1,50) foster-parenting-character",
-        "(1,51) foster-parenting-character",
-        "(1,61) foster-parenting-character",
-        "(1,62) foster-parenting-character",
-        "(1,63) foster-parenting-character",
-        "(1,71) unexpected-html-element-in-foreign-content",
-        "(1,71) foster-parenting-start-tag",
-        "(1,63) foster-parenting-character",
-        "(1,63) foster-parenting-character",
-        "(1,63) foster-parenting-character"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "math math": true,
-            "math mi": true,
-            "p": true,
-            "table": true,
-            "colgroup": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "math",
-                    "ns": "http://www.w3.org/1998/Math/MathML",
-                    "children": [
-                      {
-                        "tag": "mi",
-                        "ns": "http://www.w3.org/1998/Math/MathML",
-                        "children": [
-                          {
-                            "text": "foo"
-                          }
-                        ]
-                      },
-                      {
-                        "tag": "mi",
-                        "ns": "http://www.w3.org/1998/Math/MathML",
-                        "children": [
-                          {
-                            "text": "bar"
-                          }
-                        ]
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "p",
-                    "children": [
-                      {
-                        "text": "baz"
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "colgroup"
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "p",
-                    "children": [
-                      {
-                        "text": "quux"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><math><mi>foo</mi><mi>bar</mi></math><p>baz</p><table><colgroup></colgroup></table><p>quux</p></body></html>",
-        "noQuirksBodyHtml": "<math><mi>foo</mi><mi>bar</mi><p>baz</p></math><table><colgroup></colgroup></table><p>quux</p>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><body><table><tr><td><select><math><mi>foo</mi><mi>bar</mi><p>baz</table><p>quux",
-      "errors": [
-        "(1,50) unexpected-start-tag-in-select",
-        "(1,54) unexpected-start-tag-in-select",
-        "(1,62) unexpected-end-tag-in-select",
-        "(1,66) unexpected-start-tag-in-select",
-        "(1,74) unexpected-end-tag-in-select",
-        "(1,77) unexpected-start-tag-in-select",
-        "(1,88) unexpected-table-element-end-tag-in-select-in-table"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true,
-            "tbody": true,
-            "tr": true,
-            "td": true,
-            "select": true,
-            "p": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "tbody",
-                        "children": [
-                          {
-                            "tag": "tr",
-                            "children": [
-                              {
-                                "tag": "td",
-                                "children": [
-                                  {
-                                    "tag": "select",
-                                    "children": [
-                                      {
-                                        "text": "foobarbaz"
-                                      }
-                                    ]
-                                  }
-                                ]
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "p",
-                    "children": [
-                      {
-                        "text": "quux"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><table><tbody><tr><td><select>foobarbaz</select></td></tr></tbody></table><p>quux</p></body></html>",
-        "noQuirksBodyHtml": "<table><tbody><tr><td><select>foobarbaz</select></td></tr></tbody></table><p>quux</p>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><body><table><select><math><mi>foo</mi><mi>bar</mi><p>baz</table><p>quux",
-      "errors": [
-        "(1,36) unexpected-start-tag-implies-table-voodoo",
-        "(1,42) unexpected-start-tag-in-select",
-        "(1,46) unexpected-start-tag-in-select",
-        "(1,54) unexpected-end-tag-in-select",
-        "(1,58) unexpected-start-tag-in-select",
-        "(1,66) unexpected-end-tag-in-select",
-        "(1,69) unexpected-start-tag-in-select",
-        "(1,80) unexpected-table-element-end-tag-in-select-in-table"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "select": true,
-            "table": true,
-            "p": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "select",
-                    "children": [
-                      {
-                        "text": "foobarbaz"
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "table"
-                  },
-                  {
-                    "tag": "p",
-                    "children": [
-                      {
-                        "text": "quux"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><select>foobarbaz</select><table></table><p>quux</p></body></html>",
-        "noQuirksBodyHtml": "<select>foobarbaz</select><table></table><p>quux</p>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><body></body></html><math><mi>foo</mi><mi>bar</mi><p>baz",
-      "errors": [
-        "(1,41) expected-eof-but-got-start-tag",
-        "(1,68) unexpected-html-element-in-foreign-content"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "math math": true,
-            "math mi": true,
-            "p": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "math",
-                    "ns": "http://www.w3.org/1998/Math/MathML",
-                    "children": [
-                      {
-                        "tag": "mi",
-                        "ns": "http://www.w3.org/1998/Math/MathML",
-                        "children": [
-                          {
-                            "text": "foo"
-                          }
-                        ]
-                      },
-                      {
-                        "tag": "mi",
-                        "ns": "http://www.w3.org/1998/Math/MathML",
-                        "children": [
-                          {
-                            "text": "bar"
-                          }
-                        ]
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "p",
-                    "children": [
-                      {
-                        "text": "baz"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><math><mi>foo</mi><mi>bar</mi></math><p>baz</p></body></html>",
-        "noQuirksBodyHtml": "<math><mi>foo</mi><mi>bar</mi><p>baz</p></math>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><body></body><math><mi>foo</mi><mi>bar</mi><p>baz",
-      "errors": [
-        "(1,34) unexpected-start-tag-after-body",
-        "(1,61) unexpected-html-element-in-foreign-content"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "math math": true,
-            "math mi": true,
-            "p": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "math",
-                    "ns": "http://www.w3.org/1998/Math/MathML",
-                    "children": [
-                      {
-                        "tag": "mi",
-                        "ns": "http://www.w3.org/1998/Math/MathML",
-                        "children": [
-                          {
-                            "text": "foo"
-                          }
-                        ]
-                      },
-                      {
-                        "tag": "mi",
-                        "ns": "http://www.w3.org/1998/Math/MathML",
-                        "children": [
-                          {
-                            "text": "bar"
-                          }
-                        ]
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "p",
-                    "children": [
-                      {
-                        "text": "baz"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><math><mi>foo</mi><mi>bar</mi></math><p>baz</p></body></html>",
-        "noQuirksBodyHtml": "<math><mi>foo</mi><mi>bar</mi><p>baz</p></math>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><frameset><math><mi></mi><mi></mi><p><span>",
-      "errors": [
-        "(1,31) unexpected-start-tag-in-frameset",
-        "(1,35) unexpected-start-tag-in-frameset",
-        "(1,40) unexpected-end-tag-in-frameset",
-        "(1,44) unexpected-start-tag-in-frameset",
-        "(1,49) unexpected-end-tag-in-frameset",
-        "(1,52) unexpected-start-tag-in-frameset",
-        "(1,58) unexpected-start-tag-in-frameset",
-        "(1,58) eof-in-frameset"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "frameset": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "frameset"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><frameset></frameset></html>",
-        "noQuirksBodyHtml": "<math><mi></mi><mi></mi><p><span></span></p></math>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><frameset></frameset><math><mi></mi><mi></mi><p><span>",
-      "errors": [
-        "(1,42) unexpected-start-tag-after-frameset",
-        "(1,46) unexpected-start-tag-after-frameset",
-        "(1,51) unexpected-end-tag-after-frameset",
-        "(1,55) unexpected-start-tag-after-frameset",
-        "(1,60) unexpected-end-tag-after-frameset",
-        "(1,63) unexpected-start-tag-after-frameset",
-        "(1,69) unexpected-start-tag-after-frameset"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "frameset": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "frameset"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><frameset></frameset></html>",
-        "noQuirksBodyHtml": "<math><mi></mi><mi></mi><p><span></span></p></math>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><body xlink:href=foo><math xlink:href=foo></math>",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "math math": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "attrs": [
-                  {
-                    "name": "xlink:href",
-                    "value": "foo"
-                  }
-                ],
-                "children": [
-                  {
-                    "tag": "math",
-                    "ns": "http://www.w3.org/1998/Math/MathML",
-                    "attrs": [
-                      {
-                        "name": "href",
-                        "ns": "http://www.w3.org/1999/xlink",
-                        "value": "foo"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body xlink:href=\"foo\"><math xlink:href=\"foo\"></math></body></html>",
-        "noQuirksBodyHtml": "<math xlink:href=\"foo\"></math>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><body xlink:href=foo xml:lang=en><math><mi xml:lang=en xlink:href=foo></mi></math>",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "math math": true,
-            "math mi": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "attrs": [
-                  {
-                    "name": "xlink:href",
-                    "value": "foo"
-                  },
-                  {
-                    "name": "xml:lang",
-                    "value": "en"
-                  }
-                ],
-                "children": [
-                  {
-                    "tag": "math",
-                    "ns": "http://www.w3.org/1998/Math/MathML",
-                    "children": [
-                      {
-                        "tag": "mi",
-                        "ns": "http://www.w3.org/1998/Math/MathML",
-                        "attrs": [
-                          {
-                            "name": "href",
-                            "ns": "http://www.w3.org/1999/xlink",
-                            "value": "foo"
-                          },
-                          {
-                            "name": "lang",
-                            "ns": "http://www.w3.org/XML/1998/namespace",
-                            "value": "en"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body xlink:href=\"foo\" xml:lang=\"en\"><math><mi xml:lang=\"en\" xlink:href=\"foo\"></mi></math></body></html>",
-        "noQuirksBodyHtml": "<math><mi xml:lang=\"en\" xlink:href=\"foo\"></mi></math>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><body xlink:href=foo xml:lang=en><math><mi xml:lang=en xlink:href=foo /></math>",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "math math": true,
-            "math mi": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "attrs": [
-                  {
-                    "name": "xlink:href",
-                    "value": "foo"
-                  },
-                  {
-                    "name": "xml:lang",
-                    "value": "en"
-                  }
-                ],
-                "children": [
-                  {
-                    "tag": "math",
-                    "ns": "http://www.w3.org/1998/Math/MathML",
-                    "children": [
-                      {
-                        "tag": "mi",
-                        "ns": "http://www.w3.org/1998/Math/MathML",
-                        "attrs": [
-                          {
-                            "name": "href",
-                            "ns": "http://www.w3.org/1999/xlink",
-                            "value": "foo"
-                          },
-                          {
-                            "name": "lang",
-                            "ns": "http://www.w3.org/XML/1998/namespace",
-                            "value": "en"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body xlink:href=\"foo\" xml:lang=\"en\"><math><mi xml:lang=\"en\" xlink:href=\"foo\"></mi></math></body></html>",
-        "noQuirksBodyHtml": "<math><mi xml:lang=\"en\" xlink:href=\"foo\"></mi></math>"
-      }
-    },
-    {
-      "data": "<!DOCTYPE html><body xlink:href=foo xml:lang=en><math><mi xml:lang=en xlink:href=foo />bar</math>",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "math math": true,
-            "math mi": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "attrs": [
-                  {
-                    "name": "xlink:href",
-                    "value": "foo"
-                  },
-                  {
-                    "name": "xml:lang",
-                    "value": "en"
-                  }
-                ],
-                "children": [
-                  {
-                    "tag": "math",
-                    "ns": "http://www.w3.org/1998/Math/MathML",
-                    "children": [
-                      {
-                        "tag": "mi",
-                        "ns": "http://www.w3.org/1998/Math/MathML",
-                        "attrs": [
-                          {
-                            "name": "href",
-                            "ns": "http://www.w3.org/1999/xlink",
-                            "value": "foo"
-                          },
-                          {
-                            "name": "lang",
-                            "ns": "http://www.w3.org/XML/1998/namespace",
-                            "value": "en"
-                          }
-                        ]
-                      },
-                      {
-                        "text": "bar"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body xlink:href=\"foo\" xml:lang=\"en\"><math><mi xml:lang=\"en\" xlink:href=\"foo\"></mi>bar</math></body></html>",
-        "noQuirksBodyHtml": "<math><mi xml:lang=\"en\" xlink:href=\"foo\"></mi>bar</math>"
-      }
-    }
-  ],
-  "tests_innerHTML_1.dat": [
-    {
-      "data": "<body><span>",
-      "errors": [
-        "(1,6): unexpected-start-tag",
-        "(1,12): expected-closing-tag-but-got-eof"
-      ],
-      "fragment": {
-        "name": "body"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "span": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "span"
-          }
-        ],
-        "html": "<span></span>",
-        "noQuirksBodyHtml": "<span></span>"
-      }
-    },
-    {
-      "data": "<span><body>",
-      "errors": [
-        "(1,12): unexpected-start-tag",
-        "(1,12): expected-closing-tag-but-got-eof"
-      ],
-      "fragment": {
-        "name": "body"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "span": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "span"
-          }
-        ],
-        "html": "<span></span>",
-        "noQuirksBodyHtml": "<span></span>"
-      }
-    },
-    {
-      "data": "<span><body>",
-      "errors": [
-        "(1,12): unexpected-start-tag",
-        "(1,12): expected-closing-tag-but-got-eof"
-      ],
-      "fragment": {
-        "name": "div"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "span": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "span"
-          }
-        ],
-        "html": "<span></span>",
-        "noQuirksBodyHtml": "<span></span>"
-      }
-    },
-    {
-      "data": "<body><span>",
-      "errors": [
-        "(1,12): expected-closing-tag-but-got-eof"
-      ],
-      "fragment": {
-        "name": "html"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "head": true,
-            "body": true,
-            "span": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "head"
-          },
-          {
-            "tag": "body",
-            "children": [
-              {
-                "tag": "span"
-              }
-            ]
-          }
-        ],
-        "html": "<head></head><body><span></span></body>",
-        "noQuirksBodyHtml": "<span></span>"
-      }
-    },
-    {
-      "data": "<frameset><span>",
-      "errors": [
-        "(1,10): unexpected-start-tag",
-        "(1,16): expected-closing-tag-but-got-eof"
-      ],
-      "fragment": {
-        "name": "body"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "span": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "span"
-          }
-        ],
-        "html": "<span></span>",
-        "noQuirksBodyHtml": "<span></span>"
-      }
-    },
-    {
-      "data": "<span><frameset>",
-      "errors": [
-        "(1,16): unexpected-start-tag",
-        "(1,16): expected-closing-tag-but-got-eof"
-      ],
-      "fragment": {
-        "name": "body"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "span": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "span"
-          }
-        ],
-        "html": "<span></span>",
-        "noQuirksBodyHtml": "<span></span>"
-      }
-    },
-    {
-      "data": "<span><frameset>",
-      "errors": [
-        "(1,16): unexpected-start-tag",
-        "(1,16): expected-closing-tag-but-got-eof"
-      ],
-      "fragment": {
-        "name": "div"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "span": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "span"
-          }
-        ],
-        "html": "<span></span>",
-        "noQuirksBodyHtml": "<span></span>"
-      }
-    },
-    {
-      "data": "<frameset><span>",
-      "errors": [
-        "(1,16): unexpected-start-tag-in-frameset",
-        "(1,16): eof-in-frameset"
-      ],
-      "fragment": {
-        "name": "html"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "head": true,
-            "frameset": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "head"
-          },
-          {
-            "tag": "frameset"
-          }
-        ],
-        "html": "<head></head><frameset></frameset>",
-        "noQuirksBodyHtml": "<span></span>"
-      }
-    },
-    {
-      "data": "<table><tr>",
-      "errors": [
-        "(1,7): unexpected-start-tag"
-      ],
-      "fragment": {
-        "name": "table"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "tbody": true,
-            "tr": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "tbody",
-            "children": [
-              {
-                "tag": "tr"
-              }
-            ]
-          }
-        ],
-        "html": "<tbody><tr></tr></tbody>",
-        "noQuirksBodyHtml": "<table><tbody><tr></tr></tbody></table>"
-      }
-    },
-    {
-      "data": "</table><tr>",
-      "errors": [
-        "(1,8): unexpected-end-tag"
-      ],
-      "fragment": {
-        "name": "table"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "tbody": true,
-            "tr": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "tbody",
-            "children": [
-              {
-                "tag": "tr"
-              }
-            ]
-          }
-        ],
-        "html": "<tbody><tr></tr></tbody>",
-        "noQuirksBodyHtml": ""
-      }
-    },
-    {
-      "data": "<a>",
-      "errors": [
-        "(1,3): unexpected-start-tag-implies-table-voodoo",
-        "(1,3): eof-in-table"
-      ],
-      "fragment": {
-        "name": "table"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "a": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "a"
-          }
-        ],
-        "html": "<a></a>",
-        "noQuirksBodyHtml": "<a></a>"
-      }
-    },
-    {
-      "data": "<a>",
-      "errors": [
-        "(1,3): unexpected-start-tag-implies-table-voodoo",
-        "(1,3): eof-in-table"
-      ],
-      "fragment": {
-        "name": "table"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "a": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "a"
-          }
-        ],
-        "html": "<a></a>",
-        "noQuirksBodyHtml": "<a></a>"
-      }
-    },
-    {
-      "data": "<a><caption>a",
-      "errors": [
-        "(1,3): unexpected-start-tag-implies-table-voodoo",
-        "(1,13): expected-closing-tag-but-got-eof"
-      ],
-      "fragment": {
-        "name": "table"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "a": true,
-            "caption": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "a"
-          },
-          {
-            "tag": "caption",
-            "children": [
-              {
-                "text": "a"
-              }
-            ]
-          }
-        ],
-        "html": "<a></a><caption>a</caption>",
-        "noQuirksBodyHtml": "<a>a</a>"
-      }
-    },
-    {
-      "data": "<a><colgroup><col>",
-      "errors": [
-        "(1,3): foster-parenting-start-token",
-        "(1,18): expected-closing-tag-but-got-eof"
-      ],
-      "fragment": {
-        "name": "table"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "a": true,
-            "colgroup": true,
-            "col": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "a"
-          },
-          {
-            "tag": "colgroup",
-            "children": [
-              {
-                "tag": "col"
-              }
-            ]
-          }
-        ],
-        "html": "<a></a><colgroup><col></colgroup>",
-        "noQuirksBodyHtml": "<a></a>"
-      }
-    },
-    {
-      "data": "<a><tbody><tr>",
-      "errors": [
-        "(1,3): foster-parenting-start-tag"
-      ],
-      "fragment": {
-        "name": "table"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "a": true,
-            "tbody": true,
-            "tr": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "a"
-          },
-          {
-            "tag": "tbody",
-            "children": [
-              {
-                "tag": "tr"
-              }
-            ]
-          }
-        ],
-        "html": "<a></a><tbody><tr></tr></tbody>",
-        "noQuirksBodyHtml": "<a></a>"
-      }
-    },
-    {
-      "data": "<a><tfoot><tr>",
-      "errors": [
-        "(1,3): foster-parenting-start-tag"
-      ],
-      "fragment": {
-        "name": "table"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "a": true,
-            "tfoot": true,
-            "tr": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "a"
-          },
-          {
-            "tag": "tfoot",
-            "children": [
-              {
-                "tag": "tr"
-              }
-            ]
-          }
-        ],
-        "html": "<a></a><tfoot><tr></tr></tfoot>",
-        "noQuirksBodyHtml": "<a></a>"
-      }
-    },
-    {
-      "data": "<a><thead><tr>",
-      "errors": [
-        "(1,3): foster-parenting-start-tag"
-      ],
-      "fragment": {
-        "name": "table"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "a": true,
-            "thead": true,
-            "tr": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "a"
-          },
-          {
-            "tag": "thead",
-            "children": [
-              {
-                "tag": "tr"
-              }
-            ]
-          }
-        ],
-        "html": "<a></a><thead><tr></tr></thead>",
-        "noQuirksBodyHtml": "<a></a>"
-      }
-    },
-    {
-      "data": "<a><tr>",
-      "errors": [
-        "(1,3): foster-parenting-start-tag"
-      ],
-      "fragment": {
-        "name": "table"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "a": true,
-            "tbody": true,
-            "tr": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "a"
-          },
-          {
-            "tag": "tbody",
-            "children": [
-              {
-                "tag": "tr"
-              }
-            ]
-          }
-        ],
-        "html": "<a></a><tbody><tr></tr></tbody>",
-        "noQuirksBodyHtml": "<a></a>"
-      }
-    },
-    {
-      "data": "<a><th>",
-      "errors": [
-        "(1,3): unexpected-start-tag-implies-table-voodoo",
-        "(1,7): unexpected-cell-in-table-body"
-      ],
-      "fragment": {
-        "name": "table"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "a": true,
-            "tbody": true,
-            "tr": true,
-            "th": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "a"
-          },
-          {
-            "tag": "tbody",
-            "children": [
-              {
-                "tag": "tr",
-                "children": [
-                  {
-                    "tag": "th"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<a></a><tbody><tr><th></th></tr></tbody>",
-        "noQuirksBodyHtml": "<a></a>"
-      }
-    },
-    {
-      "data": "<a><td>",
-      "errors": [
-        "(1,3): unexpected-start-tag-implies-table-voodoo",
-        "(1,7): unexpected-cell-in-table-body"
-      ],
-      "fragment": {
-        "name": "table"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "a": true,
-            "tbody": true,
-            "tr": true,
-            "td": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "a"
-          },
-          {
-            "tag": "tbody",
-            "children": [
-              {
-                "tag": "tr",
-                "children": [
-                  {
-                    "tag": "td"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<a></a><tbody><tr><td></td></tr></tbody>",
-        "noQuirksBodyHtml": "<a></a>"
-      }
-    },
-    {
-      "data": "<table></table><tbody>",
-      "errors": [
-        "(1,22): unexpected-start-tag"
-      ],
-      "fragment": {
-        "name": "caption"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "table": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "table"
-          }
-        ],
-        "html": "<table></table>",
-        "noQuirksBodyHtml": "<table></table>"
-      }
-    },
-    {
-      "data": "</table><span>",
-      "errors": [
-        "(1,8): unexpected-end-tag",
-        "(1,14): expected-closing-tag-but-got-eof"
-      ],
-      "fragment": {
-        "name": "caption"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "span": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "span"
-          }
-        ],
-        "html": "<span></span>",
-        "noQuirksBodyHtml": "<span></span>"
-      }
-    },
-    {
-      "data": "<span></table>",
-      "errors": [
-        "(1,14): unexpected-end-tag",
-        "(1,14): expected-closing-tag-but-got-eof"
-      ],
-      "fragment": {
-        "name": "caption"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "span": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "span"
-          }
-        ],
-        "html": "<span></span>",
-        "noQuirksBodyHtml": "<span></span>"
-      }
-    },
-    {
-      "data": "</caption><span>",
-      "errors": [
-        "(1,10): XXX-undefined-error",
-        "(1,16): expected-closing-tag-but-got-eof"
-      ],
-      "fragment": {
-        "name": "caption"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "span": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "span"
-          }
-        ],
-        "html": "<span></span>",
-        "noQuirksBodyHtml": "<span></span>"
-      }
-    },
-    {
-      "data": "<span></caption><span>",
-      "errors": [
-        "(1,16): XXX-undefined-error",
-        "(1,22): expected-closing-tag-but-got-eof"
-      ],
-      "fragment": {
-        "name": "caption"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "span": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "span",
-            "children": [
-              {
-                "tag": "span"
-              }
-            ]
-          }
-        ],
-        "html": "<span><span></span></span>",
-        "noQuirksBodyHtml": "<span><span></span></span>"
-      }
-    },
-    {
-      "data": "<span><caption><span>",
-      "errors": [
-        "(1,15): unexpected-start-tag",
-        "(1,21): expected-closing-tag-but-got-eof"
-      ],
-      "fragment": {
-        "name": "caption"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "span": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "span",
-            "children": [
-              {
-                "tag": "span"
-              }
-            ]
-          }
-        ],
-        "html": "<span><span></span></span>",
-        "noQuirksBodyHtml": "<span><span></span></span>"
-      }
-    },
-    {
-      "data": "<span><col><span>",
-      "errors": [
-        "(1,11): unexpected-start-tag",
-        "(1,17): expected-closing-tag-but-got-eof"
-      ],
-      "fragment": {
-        "name": "caption"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "span": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "span",
-            "children": [
-              {
-                "tag": "span"
-              }
-            ]
-          }
-        ],
-        "html": "<span><span></span></span>",
-        "noQuirksBodyHtml": "<span><span></span></span>"
-      }
-    },
-    {
-      "data": "<span><colgroup><span>",
-      "errors": [
-        "(1,16): unexpected-start-tag",
-        "(1,22): expected-closing-tag-but-got-eof"
-      ],
-      "fragment": {
-        "name": "caption"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "span": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "span",
-            "children": [
-              {
-                "tag": "span"
-              }
-            ]
-          }
-        ],
-        "html": "<span><span></span></span>",
-        "noQuirksBodyHtml": "<span><span></span></span>"
-      }
-    },
-    {
-      "data": "<span><html><span>",
-      "errors": [
-        "(1,12): non-html-root",
-        "(1,18): expected-closing-tag-but-got-eof"
-      ],
-      "fragment": {
-        "name": "caption"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "span": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "span",
-            "children": [
-              {
-                "tag": "span"
-              }
-            ]
-          }
-        ],
-        "html": "<span><span></span></span>",
-        "noQuirksBodyHtml": "<span><span></span></span>"
-      }
-    },
-    {
-      "data": "<span><tbody><span>",
-      "errors": [
-        "(1,13): unexpected-start-tag",
-        "(1,19): expected-closing-tag-but-got-eof"
-      ],
-      "fragment": {
-        "name": "caption"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "span": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "span",
-            "children": [
-              {
-                "tag": "span"
-              }
-            ]
-          }
-        ],
-        "html": "<span><span></span></span>",
-        "noQuirksBodyHtml": "<span><span></span></span>"
-      }
-    },
-    {
-      "data": "<span><td><span>",
-      "errors": [
-        "(1,10): unexpected-start-tag",
-        "(1,16): expected-closing-tag-but-got-eof"
-      ],
-      "fragment": {
-        "name": "caption"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "span": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "span",
-            "children": [
-              {
-                "tag": "span"
-              }
-            ]
-          }
-        ],
-        "html": "<span><span></span></span>",
-        "noQuirksBodyHtml": "<span><span></span></span>"
-      }
-    },
-    {
-      "data": "<span><tfoot><span>",
-      "errors": [
-        "(1,13): unexpected-start-tag",
-        "(1,19): expected-closing-tag-but-got-eof"
-      ],
-      "fragment": {
-        "name": "caption"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "span": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "span",
-            "children": [
-              {
-                "tag": "span"
-              }
-            ]
-          }
-        ],
-        "html": "<span><span></span></span>",
-        "noQuirksBodyHtml": "<span><span></span></span>"
-      }
-    },
-    {
-      "data": "<span><thead><span>",
-      "errors": [
-        "(1,13): unexpected-start-tag",
-        "(1,19): expected-closing-tag-but-got-eof"
-      ],
-      "fragment": {
-        "name": "caption"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "span": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "span",
-            "children": [
-              {
-                "tag": "span"
-              }
-            ]
-          }
-        ],
-        "html": "<span><span></span></span>",
-        "noQuirksBodyHtml": "<span><span></span></span>"
-      }
-    },
-    {
-      "data": "<span><th><span>",
-      "errors": [
-        "(1,10): unexpected-start-tag",
-        "(1,16): expected-closing-tag-but-got-eof"
-      ],
-      "fragment": {
-        "name": "caption"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "span": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "span",
-            "children": [
-              {
-                "tag": "span"
-              }
-            ]
-          }
-        ],
-        "html": "<span><span></span></span>",
-        "noQuirksBodyHtml": "<span><span></span></span>"
-      }
-    },
-    {
-      "data": "<span><tr><span>",
-      "errors": [
-        "(1,10): unexpected-start-tag",
-        "(1,16): expected-closing-tag-but-got-eof"
-      ],
-      "fragment": {
-        "name": "caption"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "span": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "span",
-            "children": [
-              {
-                "tag": "span"
-              }
-            ]
-          }
-        ],
-        "html": "<span><span></span></span>",
-        "noQuirksBodyHtml": "<span><span></span></span>"
-      }
-    },
-    {
-      "data": "<span></table><span>",
-      "errors": [
-        "(1,14): unexpected-end-tag",
-        "(1,20): expected-closing-tag-but-got-eof"
-      ],
-      "fragment": {
-        "name": "caption"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "span": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "span",
-            "children": [
-              {
-                "tag": "span"
-              }
-            ]
-          }
-        ],
-        "html": "<span><span></span></span>",
-        "noQuirksBodyHtml": "<span><span></span></span>"
-      }
-    },
-    {
-      "data": "</colgroup><col>",
-      "errors": [
-        "(1,11): XXX-undefined-error"
-      ],
-      "fragment": {
-        "name": "colgroup"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "col": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "col"
-          }
-        ],
-        "html": "<col>",
-        "noQuirksBodyHtml": ""
-      }
-    },
-    {
-      "data": "<a><col>",
-      "errors": [
-        "(1,3): XXX-undefined-error"
-      ],
-      "fragment": {
-        "name": "colgroup"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "col": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "col"
-          }
-        ],
-        "html": "<col>",
-        "noQuirksBodyHtml": "<a></a>"
-      }
-    },
-    {
-      "data": "<caption><a>",
-      "errors": [
-        "(1,9): XXX-undefined-error",
-        "(1,12): unexpected-start-tag-implies-table-voodoo",
-        "(1,12): eof-in-table"
-      ],
-      "fragment": {
-        "name": "tbody"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "a": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "a"
-          }
-        ],
-        "html": "<a></a>",
-        "noQuirksBodyHtml": "<a></a>"
-      }
-    },
-    {
-      "data": "<col><a>",
-      "errors": [
-        "(1,5): XXX-undefined-error",
-        "(1,8): unexpected-start-tag-implies-table-voodoo",
-        "(1,8): eof-in-table"
-      ],
-      "fragment": {
-        "name": "tbody"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "a": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "a"
-          }
-        ],
-        "html": "<a></a>",
-        "noQuirksBodyHtml": "<a></a>"
-      }
-    },
-    {
-      "data": "<colgroup><a>",
-      "errors": [
-        "(1,10): XXX-undefined-error",
-        "(1,13): unexpected-start-tag-implies-table-voodoo",
-        "(1,13): eof-in-table"
-      ],
-      "fragment": {
-        "name": "tbody"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "a": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "a"
-          }
-        ],
-        "html": "<a></a>",
-        "noQuirksBodyHtml": "<a></a>"
-      }
-    },
-    {
-      "data": "<tbody><a>",
-      "errors": [
-        "(1,7): XXX-undefined-error",
-        "(1,10): unexpected-start-tag-implies-table-voodoo",
-        "(1,10): eof-in-table"
-      ],
-      "fragment": {
-        "name": "tbody"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "a": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "a"
-          }
-        ],
-        "html": "<a></a>",
-        "noQuirksBodyHtml": "<a></a>"
-      }
-    },
-    {
-      "data": "<tfoot><a>",
-      "errors": [
-        "(1,7): XXX-undefined-error",
-        "(1,10): unexpected-start-tag-implies-table-voodoo",
-        "(1,10): eof-in-table"
-      ],
-      "fragment": {
-        "name": "tbody"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "a": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "a"
-          }
-        ],
-        "html": "<a></a>",
-        "noQuirksBodyHtml": "<a></a>"
-      }
-    },
-    {
-      "data": "<thead><a>",
-      "errors": [
-        "(1,7): XXX-undefined-error",
-        "(1,10): unexpected-start-tag-implies-table-voodoo",
-        "(1,10): eof-in-table"
-      ],
-      "fragment": {
-        "name": "tbody"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "a": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "a"
-          }
-        ],
-        "html": "<a></a>",
-        "noQuirksBodyHtml": "<a></a>"
-      }
-    },
-    {
-      "data": "</table><a>",
-      "errors": [
-        "(1,8): XXX-undefined-error",
-        "(1,11): unexpected-start-tag-implies-table-voodoo",
-        "(1,11): eof-in-table"
-      ],
-      "fragment": {
-        "name": "tbody"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "a": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "a"
-          }
-        ],
-        "html": "<a></a>",
-        "noQuirksBodyHtml": "<a></a>"
-      }
-    },
-    {
-      "data": "<a><tr>",
-      "errors": [
-        "(1,3): unexpected-start-tag-implies-table-voodoo"
-      ],
-      "fragment": {
-        "name": "tbody"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "a": true,
-            "tr": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "a"
-          },
-          {
-            "tag": "tr"
-          }
-        ],
-        "html": "<a></a><tr></tr>",
-        "noQuirksBodyHtml": "<a></a>"
-      }
-    },
-    {
-      "data": "<a><td>",
-      "errors": [
-        "(1,3): unexpected-start-tag-implies-table-voodoo",
-        "(1,7): unexpected-cell-in-table-body"
-      ],
-      "fragment": {
-        "name": "tbody"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "a": true,
-            "tr": true,
-            "td": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "a"
-          },
-          {
-            "tag": "tr",
-            "children": [
-              {
-                "tag": "td"
-              }
-            ]
-          }
-        ],
-        "html": "<a></a><tr><td></td></tr>",
-        "noQuirksBodyHtml": "<a></a>"
-      }
-    },
-    {
-      "data": "<a><td>",
-      "errors": [
-        "(1,3): unexpected-start-tag-implies-table-voodoo",
-        "(1,7): unexpected-cell-in-table-body"
-      ],
-      "fragment": {
-        "name": "tbody"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "a": true,
-            "tr": true,
-            "td": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "a"
-          },
-          {
-            "tag": "tr",
-            "children": [
-              {
-                "tag": "td"
-              }
-            ]
-          }
-        ],
-        "html": "<a></a><tr><td></td></tr>",
-        "noQuirksBodyHtml": "<a></a>"
-      }
-    },
-    {
-      "data": "<a><td>",
-      "errors": [
-        "(1,3): unexpected-start-tag-implies-table-voodoo",
-        "(1,7): unexpected-cell-in-table-body"
-      ],
-      "fragment": {
-        "name": "tbody"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "a": true,
-            "tr": true,
-            "td": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "a"
-          },
-          {
-            "tag": "tr",
-            "children": [
-              {
-                "tag": "td"
-              }
-            ]
-          }
-        ],
-        "html": "<a></a><tr><td></td></tr>",
-        "noQuirksBodyHtml": "<a></a>"
-      }
-    },
-    {
-      "data": "<td><table><tbody><a><tr>",
-      "errors": [
-        "(1,4): unexpected-cell-in-table-body",
-        "(1,21): unexpected-start-tag-implies-table-voodoo",
-        "(1,25): eof-in-table"
-      ],
-      "fragment": {
-        "name": "tbody"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "tr": true,
-            "td": true,
-            "a": true,
-            "table": true,
-            "tbody": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "tr",
-            "children": [
-              {
-                "tag": "td",
-                "children": [
-                  {
-                    "tag": "a"
-                  },
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "tbody",
-                        "children": [
-                          {
-                            "tag": "tr"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<tr><td><a></a><table><tbody><tr></tr></tbody></table></td></tr>",
-        "noQuirksBodyHtml": "<a></a><table><tbody><tr></tr></tbody></table>"
-      }
-    },
-    {
-      "data": "</tr><td>",
-      "errors": [
-        "(1,5): XXX-undefined-error"
-      ],
-      "fragment": {
-        "name": "tr"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "td": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "td"
-          }
-        ],
-        "html": "<td></td>",
-        "noQuirksBodyHtml": ""
-      }
-    },
-    {
-      "data": "<td><table><a><tr></tr><tr>",
-      "errors": [
-        "(1,14): unexpected-start-tag-implies-table-voodoo",
-        "(1,27): eof-in-table"
-      ],
-      "fragment": {
-        "name": "tr"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "td": true,
-            "a": true,
-            "table": true,
-            "tbody": true,
-            "tr": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "td",
-            "children": [
-              {
-                "tag": "a"
-              },
-              {
-                "tag": "table",
-                "children": [
-                  {
-                    "tag": "tbody",
-                    "children": [
-                      {
-                        "tag": "tr"
-                      },
-                      {
-                        "tag": "tr"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<td><a></a><table><tbody><tr></tr><tr></tr></tbody></table></td>",
-        "noQuirksBodyHtml": "<a></a><table><tbody><tr></tr><tr></tr></tbody></table>"
-      }
-    },
-    {
-      "data": "<caption><td>",
-      "errors": [
-        "(1,9): XXX-undefined-error"
-      ],
-      "fragment": {
-        "name": "tr"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "td": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "td"
-          }
-        ],
-        "html": "<td></td>",
-        "noQuirksBodyHtml": ""
-      }
-    },
-    {
-      "data": "<col><td>",
-      "errors": [
-        "(1,5): XXX-undefined-error"
-      ],
-      "fragment": {
-        "name": "tr"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "td": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "td"
-          }
-        ],
-        "html": "<td></td>",
-        "noQuirksBodyHtml": ""
-      }
-    },
-    {
-      "data": "<colgroup><td>",
-      "errors": [
-        "(1,10): XXX-undefined-error"
-      ],
-      "fragment": {
-        "name": "tr"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "td": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "td"
-          }
-        ],
-        "html": "<td></td>",
-        "noQuirksBodyHtml": ""
-      }
-    },
-    {
-      "data": "<tbody><td>",
-      "errors": [
-        "(1,7): XXX-undefined-error"
-      ],
-      "fragment": {
-        "name": "tr"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "td": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "td"
-          }
-        ],
-        "html": "<td></td>",
-        "noQuirksBodyHtml": ""
-      }
-    },
-    {
-      "data": "<tfoot><td>",
-      "errors": [
-        "(1,7): XXX-undefined-error"
-      ],
-      "fragment": {
-        "name": "tr"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "td": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "td"
-          }
-        ],
-        "html": "<td></td>",
-        "noQuirksBodyHtml": ""
-      }
-    },
-    {
-      "data": "<thead><td>",
-      "errors": [
-        "(1,7): XXX-undefined-error"
-      ],
-      "fragment": {
-        "name": "tr"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "td": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "td"
-          }
-        ],
-        "html": "<td></td>",
-        "noQuirksBodyHtml": ""
-      }
-    },
-    {
-      "data": "<tr><td>",
-      "errors": [
-        "(1,4): XXX-undefined-error"
-      ],
-      "fragment": {
-        "name": "tr"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "td": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "td"
-          }
-        ],
-        "html": "<td></td>",
-        "noQuirksBodyHtml": ""
-      }
-    },
-    {
-      "data": "</table><td>",
-      "errors": [
-        "(1,8): XXX-undefined-error"
-      ],
-      "fragment": {
-        "name": "tr"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "td": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "td"
-          }
-        ],
-        "html": "<td></td>",
-        "noQuirksBodyHtml": ""
-      }
-    },
-    {
-      "data": "<td><table></table><td>",
-      "errors": [],
-      "fragment": {
-        "name": "tr"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "td": true,
-            "table": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "td",
-            "children": [
-              {
-                "tag": "table"
-              }
-            ]
-          },
-          {
-            "tag": "td"
-          }
-        ],
-        "html": "<td><table></table></td><td></td>",
-        "noQuirksBodyHtml": "<table></table>"
-      }
-    },
-    {
-      "data": "<td><table></table><td>",
-      "errors": [],
-      "fragment": {
-        "name": "tr"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "td": true,
-            "table": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "td",
-            "children": [
-              {
-                "tag": "table"
-              }
-            ]
-          },
-          {
-            "tag": "td"
-          }
-        ],
-        "html": "<td><table></table></td><td></td>",
-        "noQuirksBodyHtml": "<table></table>"
-      }
-    },
-    {
-      "data": "<caption><a>",
-      "errors": [
-        "(1,9): XXX-undefined-error",
-        "(1,12): expected-closing-tag-but-got-eof"
-      ],
-      "fragment": {
-        "name": "td"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "a": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "a"
-          }
-        ],
-        "html": "<a></a>",
-        "noQuirksBodyHtml": "<a></a>"
-      }
-    },
-    {
-      "data": "<col><a>",
-      "errors": [
-        "(1,5): XXX-undefined-error",
-        "(1,8): expected-closing-tag-but-got-eof"
-      ],
-      "fragment": {
-        "name": "td"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "a": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "a"
-          }
-        ],
-        "html": "<a></a>",
-        "noQuirksBodyHtml": "<a></a>"
-      }
-    },
-    {
-      "data": "<colgroup><a>",
-      "errors": [
-        "(1,10): XXX-undefined-error",
-        "(1,13): expected-closing-tag-but-got-eof"
-      ],
-      "fragment": {
-        "name": "td"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "a": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "a"
-          }
-        ],
-        "html": "<a></a>",
-        "noQuirksBodyHtml": "<a></a>"
-      }
-    },
-    {
-      "data": "<tbody><a>",
-      "errors": [
-        "(1,7): XXX-undefined-error",
-        "(1,10): expected-closing-tag-but-got-eof"
-      ],
-      "fragment": {
-        "name": "td"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "a": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "a"
-          }
-        ],
-        "html": "<a></a>",
-        "noQuirksBodyHtml": "<a></a>"
-      }
-    },
-    {
-      "data": "<tfoot><a>",
-      "errors": [
-        "(1,7): XXX-undefined-error",
-        "(1,10): expected-closing-tag-but-got-eof"
-      ],
-      "fragment": {
-        "name": "td"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "a": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "a"
-          }
-        ],
-        "html": "<a></a>",
-        "noQuirksBodyHtml": "<a></a>"
-      }
-    },
-    {
-      "data": "<th><a>",
-      "errors": [
-        "(1,4): XXX-undefined-error",
-        "(1,7): expected-closing-tag-but-got-eof"
-      ],
-      "fragment": {
-        "name": "td"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "a": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "a"
-          }
-        ],
-        "html": "<a></a>",
-        "noQuirksBodyHtml": "<a></a>"
-      }
-    },
-    {
-      "data": "<thead><a>",
-      "errors": [
-        "(1,7): XXX-undefined-error",
-        "(1,10): expected-closing-tag-but-got-eof"
-      ],
-      "fragment": {
-        "name": "td"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "a": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "a"
-          }
-        ],
-        "html": "<a></a>",
-        "noQuirksBodyHtml": "<a></a>"
-      }
-    },
-    {
-      "data": "<tr><a>",
-      "errors": [
-        "(1,4): XXX-undefined-error",
-        "(1,7): expected-closing-tag-but-got-eof"
-      ],
-      "fragment": {
-        "name": "td"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "a": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "a"
-          }
-        ],
-        "html": "<a></a>",
-        "noQuirksBodyHtml": "<a></a>"
-      }
-    },
-    {
-      "data": "</table><a>",
-      "errors": [
-        "(1,8): XXX-undefined-error",
-        "(1,11): expected-closing-tag-but-got-eof"
-      ],
-      "fragment": {
-        "name": "td"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "a": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "a"
-          }
-        ],
-        "html": "<a></a>",
-        "noQuirksBodyHtml": "<a></a>"
-      }
-    },
-    {
-      "data": "</tbody><a>",
-      "errors": [
-        "(1,8): XXX-undefined-error",
-        "(1,11): expected-closing-tag-but-got-eof"
-      ],
-      "fragment": {
-        "name": "td"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "a": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "a"
-          }
-        ],
-        "html": "<a></a>",
-        "noQuirksBodyHtml": "<a></a>"
-      }
-    },
-    {
-      "data": "</td><a>",
-      "errors": [
-        "(1,5): unexpected-end-tag",
-        "(1,8): expected-closing-tag-but-got-eof"
-      ],
-      "fragment": {
-        "name": "td"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "a": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "a"
-          }
-        ],
-        "html": "<a></a>",
-        "noQuirksBodyHtml": "<a></a>"
-      }
-    },
-    {
-      "data": "</tfoot><a>",
-      "errors": [
-        "(1,8): XXX-undefined-error",
-        "(1,11): expected-closing-tag-but-got-eof"
-      ],
-      "fragment": {
-        "name": "td"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "a": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "a"
-          }
-        ],
-        "html": "<a></a>",
-        "noQuirksBodyHtml": "<a></a>"
-      }
-    },
-    {
-      "data": "</thead><a>",
-      "errors": [
-        "(1,8): XXX-undefined-error",
-        "(1,11): expected-closing-tag-but-got-eof"
-      ],
-      "fragment": {
-        "name": "td"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "a": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "a"
-          }
-        ],
-        "html": "<a></a>",
-        "noQuirksBodyHtml": "<a></a>"
-      }
-    },
-    {
-      "data": "</th><a>",
-      "errors": [
-        "(1,5): unexpected-end-tag",
-        "(1,8): expected-closing-tag-but-got-eof"
-      ],
-      "fragment": {
-        "name": "td"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "a": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "a"
-          }
-        ],
-        "html": "<a></a>",
-        "noQuirksBodyHtml": "<a></a>"
-      }
-    },
-    {
-      "data": "</tr><a>",
-      "errors": [
-        "(1,5): XXX-undefined-error",
-        "(1,8): expected-closing-tag-but-got-eof"
-      ],
-      "fragment": {
-        "name": "td"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "a": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "a"
-          }
-        ],
-        "html": "<a></a>",
-        "noQuirksBodyHtml": "<a></a>"
-      }
-    },
-    {
-      "data": "<table><td><td>",
-      "errors": [
-        "(1,11): unexpected-cell-in-table-body",
-        "(1,15): expected-closing-tag-but-got-eof"
-      ],
-      "fragment": {
-        "name": "td"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "table": true,
-            "tbody": true,
-            "tr": true,
-            "td": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "table",
-            "children": [
-              {
-                "tag": "tbody",
-                "children": [
-                  {
-                    "tag": "tr",
-                    "children": [
-                      {
-                        "tag": "td"
-                      },
-                      {
-                        "tag": "td"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<table><tbody><tr><td></td><td></td></tr></tbody></table>",
-        "noQuirksBodyHtml": "<table><tbody><tr><td></td><td></td></tr></tbody></table>"
-      }
-    },
-    {
-      "data": "</select><option>",
-      "errors": [
-        "(1,9): XXX-undefined-error"
-      ],
-      "fragment": {
-        "name": "select"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "option": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "option"
-          }
-        ],
-        "html": "<option></option>",
-        "noQuirksBodyHtml": "<option></option>"
-      }
-    },
-    {
-      "data": "<input><option>",
-      "errors": [
-        "(1,7): unexpected-input-in-select"
-      ],
-      "fragment": {
-        "name": "select"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "option": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "option"
-          }
-        ],
-        "html": "<option></option>",
-        "noQuirksBodyHtml": "<input><option></option>"
-      }
-    },
-    {
-      "data": "<keygen><option>",
-      "errors": [
-        "(1,8): unexpected-input-in-select"
-      ],
-      "fragment": {
-        "name": "select"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "option": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "option"
-          }
-        ],
-        "html": "<option></option>",
-        "noQuirksBodyHtml": "<keygen><option></option>"
-      }
-    },
-    {
-      "data": "<textarea><option>",
-      "errors": [
-        "(1,10): unexpected-input-in-select"
-      ],
-      "fragment": {
-        "name": "select"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "option": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "option"
-          }
-        ],
-        "html": "<option></option>",
-        "noQuirksBodyHtml": "<textarea>&lt;option&gt;</textarea>"
-      }
-    },
-    {
-      "data": "</html><!--abc-->",
-      "errors": [
-        "(1,7): unexpected-end-tag-after-body-innerhtml"
-      ],
-      "fragment": {
-        "name": "html"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "head": true,
-            "body": true
-          },
-          "comment": true
-        },
-        "tree": [
-          {
-            "tag": "head"
-          },
-          {
-            "tag": "body"
-          },
-          {
-            "comment": "abc"
-          }
-        ],
-        "html": "<head></head><body></body><!--abc-->",
-        "noQuirksBodyHtml": "<!--abc-->"
-      }
-    },
-    {
-      "data": "</frameset><frame>",
-      "errors": [
-        "(1,11): unexpected-frameset-in-frameset-innerhtml"
-      ],
-      "fragment": {
-        "name": "frameset"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "frame": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "frame"
-          }
-        ],
-        "html": "<frame>",
-        "noQuirksBodyHtml": ""
-      }
-    },
-    {
-      "data": "",
-      "errors": [],
-      "fragment": {
-        "name": "html"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "head": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "head"
-          },
-          {
-            "tag": "body"
-          }
-        ],
-        "html": "<head></head><body></body>",
-        "noQuirksBodyHtml": ""
-      }
-    }
-  ],
-  "tricky01.dat": [
-    {
-      "data": "<b><p>Bold </b> Not bold</p>\nAlso not bold.",
-      "errors": [
-        "(1,3): expected-doctype-but-got-start-tag",
-        "(1,15): adoption-agency-1.3"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "b": true,
-            "p": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "b"
-                  },
-                  {
-                    "tag": "p",
-                    "children": [
-                      {
-                        "tag": "b",
-                        "children": [
-                          {
-                            "text": "Bold "
-                          }
-                        ]
-                      },
-                      {
-                        "text": " Not bold"
-                      }
-                    ]
-                  },
-                  {
-                    "text": "\nAlso not bold."
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><b></b><p><b>Bold </b> Not bold</p>\nAlso not bold.</body></html>",
-        "noQuirksBodyHtml": "<b></b><p><b>Bold </b> Not bold</p>\nAlso not bold."
-      }
-    },
-    {
-      "data": "<html>\n<font color=red><i>Italic and Red<p>Italic and Red </font> Just italic.</p> Italic only.</i> Plain\n<p>I should not be red. <font color=red>Red. <i>Italic and red.</p>\n<p>Italic and red. </i> Red.</font> I should not be red.</p>\n<b>Bold <i>Bold and italic</b> Only Italic </i> Plain",
-      "errors": [
-        "(1,6): expected-doctype-but-got-start-tag",
-        "(2,58): adoption-agency-1.3",
-        "(3,67): unexpected-end-tag",
-        "(4,23): adoption-agency-1.3",
-        "(4,35): adoption-agency-1.3",
-        "(5,30): adoption-agency-1.3"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "font": true,
-            "i": true,
-            "p": true,
-            "b": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "font",
-                    "attrs": [
-                      {
-                        "name": "color",
-                        "value": "red"
-                      }
-                    ],
-                    "children": [
-                      {
-                        "tag": "i",
-                        "children": [
-                          {
-                            "text": "Italic and Red"
-                          }
-                        ]
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "i",
-                    "children": [
-                      {
-                        "tag": "p",
-                        "children": [
-                          {
-                            "tag": "font",
-                            "attrs": [
-                              {
-                                "name": "color",
-                                "value": "red"
-                              }
-                            ],
-                            "children": [
-                              {
-                                "text": "Italic and Red "
-                              }
-                            ]
-                          },
-                          {
-                            "text": " Just italic."
-                          }
-                        ]
-                      },
-                      {
-                        "text": " Italic only."
-                      }
-                    ]
-                  },
-                  {
-                    "text": " Plain\n"
-                  },
-                  {
-                    "tag": "p",
-                    "children": [
-                      {
-                        "text": "I should not be red. "
-                      },
-                      {
-                        "tag": "font",
-                        "attrs": [
-                          {
-                            "name": "color",
-                            "value": "red"
-                          }
-                        ],
-                        "children": [
-                          {
-                            "text": "Red. "
-                          },
-                          {
-                            "tag": "i",
-                            "children": [
-                              {
-                                "text": "Italic and red."
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "font",
-                    "attrs": [
-                      {
-                        "name": "color",
-                        "value": "red"
-                      }
-                    ],
-                    "children": [
-                      {
-                        "tag": "i",
-                        "children": [
-                          {
-                            "text": "\n"
-                          }
-                        ]
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "p",
-                    "children": [
-                      {
-                        "tag": "font",
-                        "attrs": [
-                          {
-                            "name": "color",
-                            "value": "red"
-                          }
-                        ],
-                        "children": [
-                          {
-                            "tag": "i",
-                            "children": [
-                              {
-                                "text": "Italic and red. "
-                              }
-                            ]
-                          },
-                          {
-                            "text": " Red."
-                          }
-                        ]
-                      },
-                      {
-                        "text": " I should not be red."
-                      }
-                    ]
-                  },
-                  {
-                    "text": "\n"
-                  },
-                  {
-                    "tag": "b",
-                    "children": [
-                      {
-                        "text": "Bold "
-                      },
-                      {
-                        "tag": "i",
-                        "children": [
-                          {
-                            "text": "Bold and italic"
-                          }
-                        ]
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "i",
-                    "children": [
-                      {
-                        "text": " Only Italic "
-                      }
-                    ]
-                  },
-                  {
-                    "text": " Plain"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><font color=\"red\"><i>Italic and Red</i></font><i><p><font color=\"red\">Italic and Red </font> Just italic.</p> Italic only.</i> Plain\n<p>I should not be red. <font color=\"red\">Red. <i>Italic and red.</i></font></p><font color=\"red\"><i>\n</i></font><p><font color=\"red\"><i>Italic and red. </i> Red.</font> I should not be red.</p>\n<b>Bold <i>Bold and italic</i></b><i> Only Italic </i> Plain</body></html>",
-        "noQuirksBodyHtml": "\n<font color=\"red\"><i>Italic and Red</i></font><i><p><font color=\"red\">Italic and Red </font> Just italic.</p> Italic only.</i> Plain\n<p>I should not be red. <font color=\"red\">Red. <i>Italic and red.</i></font></p><font color=\"red\"><i>\n</i></font><p><font color=\"red\"><i>Italic and red. </i> Red.</font> I should not be red.</p>\n<b>Bold <i>Bold and italic</i></b><i> Only Italic </i> Plain"
-      }
-    },
-    {
-      "data": "<html><body>\n<p><font size=\"7\">First paragraph.</p>\n<p>Second paragraph.</p></font>\n<b><p><i>Bold and Italic</b> Italic</p>",
-      "errors": [
-        "(1,6): expected-doctype-but-got-start-tag",
-        "(2,38): unexpected-end-tag",
-        "(4,28): adoption-agency-1.3",
-        "(4,28): adoption-agency-1.3",
-        "(4,39): unexpected-end-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "p": true,
-            "font": true,
-            "b": true,
-            "i": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "\n"
-                  },
-                  {
-                    "tag": "p",
-                    "children": [
-                      {
-                        "tag": "font",
-                        "attrs": [
-                          {
-                            "name": "size",
-                            "value": "7"
-                          }
-                        ],
-                        "children": [
-                          {
-                            "text": "First paragraph."
-                          }
-                        ]
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "font",
-                    "attrs": [
-                      {
-                        "name": "size",
-                        "value": "7"
-                      }
-                    ],
-                    "children": [
-                      {
-                        "text": "\n"
-                      },
-                      {
-                        "tag": "p",
-                        "children": [
-                          {
-                            "text": "Second paragraph."
-                          }
-                        ]
-                      }
-                    ]
-                  },
-                  {
-                    "text": "\n"
-                  },
-                  {
-                    "tag": "b"
-                  },
-                  {
-                    "tag": "p",
-                    "children": [
-                      {
-                        "tag": "b",
-                        "children": [
-                          {
-                            "tag": "i",
-                            "children": [
-                              {
-                                "text": "Bold and Italic"
-                              }
-                            ]
-                          }
-                        ]
-                      },
-                      {
-                        "tag": "i",
-                        "children": [
-                          {
-                            "text": " Italic"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>\n<p><font size=\"7\">First paragraph.</font></p><font size=\"7\">\n<p>Second paragraph.</p></font>\n<b></b><p><b><i>Bold and Italic</i></b><i> Italic</i></p></body></html>",
-        "noQuirksBodyHtml": "\n<p><font size=\"7\">First paragraph.</font></p><font size=\"7\">\n<p>Second paragraph.</p></font>\n<b></b><p><b><i>Bold and Italic</i></b><i> Italic</i></p>"
-      }
-    },
-    {
-      "data": "<html>\n<dl>\n<dt><b>Boo\n<dd>Goo?\n</dl>\n</html>",
-      "errors": [
-        "(1,6): expected-doctype-but-got-start-tag",
-        "(4,4): end-tag-too-early",
-        "(5,5): end-tag-too-early",
-        "(6,7): expected-one-end-tag-but-got-another"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "dl": true,
-            "dt": true,
-            "b": true,
-            "dd": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "dl",
-                    "children": [
-                      {
-                        "text": "\n"
-                      },
-                      {
-                        "tag": "dt",
-                        "children": [
-                          {
-                            "tag": "b",
-                            "children": [
-                              {
-                                "text": "Boo\n"
-                              }
-                            ]
-                          }
-                        ]
-                      },
-                      {
-                        "tag": "dd",
-                        "children": [
-                          {
-                            "tag": "b",
-                            "children": [
-                              {
-                                "text": "Goo?\n"
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "b",
-                    "children": [
-                      {
-                        "text": "\n"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><dl>\n<dt><b>Boo\n</b></dt><dd><b>Goo?\n</b></dd></dl><b>\n</b></body></html>",
-        "noQuirksBodyHtml": "\n<dl>\n<dt><b>Boo\n</b></dt><dd><b>Goo?\n</b></dd></dl><b>\n</b>"
-      }
-    },
-    {
-      "data": "<html><body>\n<label><a><div>Hello<div>World</div></a></label>  \n</body></html>",
-      "errors": [
-        "(1,6): expected-doctype-but-got-start-tag",
-        "(2,40): adoption-agency-1.3",
-        "(2,48): unexpected-end-tag",
-        "(3,7): expected-one-end-tag-but-got-another"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "label": true,
-            "a": true,
-            "div": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "\n"
-                  },
-                  {
-                    "tag": "label",
-                    "children": [
-                      {
-                        "tag": "a"
-                      },
-                      {
-                        "tag": "div",
-                        "children": [
-                          {
-                            "tag": "a",
-                            "children": [
-                              {
-                                "text": "Hello"
-                              },
-                              {
-                                "tag": "div",
-                                "children": [
-                                  {
-                                    "text": "World"
-                                  }
-                                ]
-                              }
-                            ]
-                          },
-                          {
-                            "text": "  \n"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>\n<label><a></a><div><a>Hello<div>World</div></a>  \n</div></label></body></html>",
-        "noQuirksBodyHtml": "\n<label><a></a><div><a>Hello<div>World</div></a>  \n</div></label>"
-      }
-    },
-    {
-      "data": "<table><center> <font>a</center> <img> <tr><td> </td> </tr> </table>",
-      "errors": [
-        "(1,7): expected-doctype-but-got-start-tag",
-        "(1,15): foster-parenting-start-tag",
-        "(1,16): foster-parenting-character",
-        "(1,22): foster-parenting-start-tag",
-        "(1,23): foster-parenting-character",
-        "(1,32): foster-parenting-end-tag",
-        "(1,32): end-tag-too-early",
-        "(1,33): foster-parenting-character",
-        "(1,38): foster-parenting-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "center": true,
-            "font": true,
-            "img": true,
-            "table": true,
-            "tbody": true,
-            "tr": true,
-            "td": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "center",
-                    "children": [
-                      {
-                        "text": " "
-                      },
-                      {
-                        "tag": "font",
-                        "children": [
-                          {
-                            "text": "a"
-                          }
-                        ]
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "font",
-                    "children": [
-                      {
-                        "tag": "img"
-                      },
-                      {
-                        "text": " "
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "text": " "
-                      },
-                      {
-                        "tag": "tbody",
-                        "children": [
-                          {
-                            "tag": "tr",
-                            "children": [
-                              {
-                                "tag": "td",
-                                "children": [
-                                  {
-                                    "text": " "
-                                  }
-                                ]
-                              },
-                              {
-                                "text": " "
-                              }
-                            ]
-                          },
-                          {
-                            "text": " "
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><center> <font>a</font></center><font><img> </font><table> <tbody><tr><td> </td> </tr> </tbody></table></body></html>",
-        "noQuirksBodyHtml": "<center> <font>a</font></center><font><img> </font><table> <tbody><tr><td> </td> </tr> </tbody></table>"
-      }
-    },
-    {
-      "data": "<table><tr><p><a><p>You should see this text.",
-      "errors": [
-        "(1,7): expected-doctype-but-got-start-tag",
-        "(1,14): unexpected-start-tag-implies-table-voodoo",
-        "(1,17): unexpected-start-tag-implies-table-voodoo",
-        "(1,20): unexpected-start-tag-implies-table-voodoo",
-        "(1,20): closing-non-current-p-element",
-        "(1,21): foster-parenting-character",
-        "(1,22): foster-parenting-character",
-        "(1,23): foster-parenting-character",
-        "(1,24): foster-parenting-character",
-        "(1,25): foster-parenting-character",
-        "(1,26): foster-parenting-character",
-        "(1,27): foster-parenting-character",
-        "(1,28): foster-parenting-character",
-        "(1,29): foster-parenting-character",
-        "(1,30): foster-parenting-character",
-        "(1,31): foster-parenting-character",
-        "(1,32): foster-parenting-character",
-        "(1,33): foster-parenting-character",
-        "(1,34): foster-parenting-character",
-        "(1,35): foster-parenting-character",
-        "(1,36): foster-parenting-character",
-        "(1,37): foster-parenting-character",
-        "(1,38): foster-parenting-character",
-        "(1,39): foster-parenting-character",
-        "(1,40): foster-parenting-character",
-        "(1,41): foster-parenting-character",
-        "(1,42): foster-parenting-character",
-        "(1,43): foster-parenting-character",
-        "(1,44): foster-parenting-character",
-        "(1,45): foster-parenting-character",
-        "(1,45): eof-in-table"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "p": true,
-            "a": true,
-            "table": true,
-            "tbody": true,
-            "tr": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "p",
-                    "children": [
-                      {
-                        "tag": "a"
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "p",
-                    "children": [
-                      {
-                        "tag": "a",
-                        "children": [
-                          {
-                            "text": "You should see this text."
-                          }
-                        ]
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "tbody",
-                        "children": [
-                          {
-                            "tag": "tr"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><p><a></a></p><p><a>You should see this text.</a></p><table><tbody><tr></tr></tbody></table></body></html>",
-        "noQuirksBodyHtml": "<p><a></a></p><p><a>You should see this text.</a></p><table><tbody><tr></tr></tbody></table>"
-      }
-    },
-    {
-      "data": "<TABLE>\n<TR>\n<CENTER><CENTER><TD></TD></TR><TR>\n<FONT>\n<TABLE><tr></tr></TABLE>\n</P>\n<a></font><font></a>\nThis page contains an insanely badly-nested tag sequence.",
-      "errors": [
-        "(1,7): expected-doctype-but-got-start-tag",
-        "(3,8): unexpected-start-tag-implies-table-voodoo",
-        "(3,16): unexpected-start-tag-implies-table-voodoo",
-        "(4,6): unexpected-start-tag-implies-table-voodoo",
-        "(4,6): unexpected character token in table (the newline)",
-        "(5,7): unexpected-start-tag-implies-end-tag",
-        "(6,4): unexpected p end tag",
-        "(7,10): adoption-agency-1.3",
-        "(7,20): adoption-agency-1.3",
-        "(8,57): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "center": true,
-            "font": true,
-            "table": true,
-            "tbody": true,
-            "tr": true,
-            "td": true,
-            "p": true,
-            "a": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "center",
-                    "children": [
-                      {
-                        "tag": "center"
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "font",
-                    "children": [
-                      {
-                        "text": "\n"
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "text": "\n"
-                      },
-                      {
-                        "tag": "tbody",
-                        "children": [
-                          {
-                            "tag": "tr",
-                            "children": [
-                              {
-                                "text": "\n"
-                              },
-                              {
-                                "tag": "td"
-                              }
-                            ]
-                          },
-                          {
-                            "tag": "tr",
-                            "children": [
-                              {
-                                "text": "\n"
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "tbody",
-                        "children": [
-                          {
-                            "tag": "tr"
-                          }
-                        ]
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "font",
-                    "children": [
-                      {
-                        "text": "\n"
-                      },
-                      {
-                        "tag": "p"
-                      },
-                      {
-                        "text": "\n"
-                      },
-                      {
-                        "tag": "a"
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "a",
-                    "children": [
-                      {
-                        "tag": "font"
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "font",
-                    "children": [
-                      {
-                        "text": "\nThis page contains an insanely badly-nested tag sequence."
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><center><center></center></center><font>\n</font><table>\n<tbody><tr>\n<td></td></tr><tr>\n</tr></tbody></table><table><tbody><tr></tr></tbody></table><font>\n<p></p>\n<a></a></font><a><font></font></a><font>\nThis page contains an insanely badly-nested tag sequence.</font></body></html>",
-        "noQuirksBodyHtml": "<center><center></center></center><font>\n</font><table>\n<tbody><tr>\n<td></td></tr><tr>\n</tr></tbody></table><table><tbody><tr></tr></tbody></table><font>\n<p></p>\n<a></a></font><a><font></font></a><font>\nThis page contains an insanely badly-nested tag sequence.</font>"
-      }
-    },
-    {
-      "data": "<html>\n<body>\n<b><nobr><div>This text is in a div inside a nobr</nobr>More text that should not be in the nobr, i.e., the\nnobr should have closed the div inside it implicitly. </b><pre>A pre tag outside everything else.</pre>\n</body>\n</html>",
-      "errors": [
-        "(1,6): expected-doctype-but-got-start-tag",
-        "(3,56): adoption-agency-1.3",
-        "(4,58): adoption-agency-1.3",
-        "(5,7): expected-one-end-tag-but-got-another"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "b": true,
-            "nobr": true,
-            "div": true,
-            "pre": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "\n"
-                  },
-                  {
-                    "tag": "b",
-                    "children": [
-                      {
-                        "tag": "nobr"
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "div",
-                    "children": [
-                      {
-                        "tag": "b",
-                        "children": [
-                          {
-                            "tag": "nobr",
-                            "children": [
-                              {
-                                "text": "This text is in a div inside a nobr"
-                              }
-                            ]
-                          },
-                          {
-                            "text": "More text that should not be in the nobr, i.e., the\nnobr should have closed the div inside it implicitly. "
-                          }
-                        ]
-                      },
-                      {
-                        "tag": "pre",
-                        "children": [
-                          {
-                            "text": "A pre tag outside everything else."
-                          }
-                        ]
-                      },
-                      {
-                        "text": "\n\n"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>\n<b><nobr></nobr></b><div><b><nobr>This text is in a div inside a nobr</nobr>More text that should not be in the nobr, i.e., the\nnobr should have closed the div inside it implicitly. </b><pre>A pre tag outside everything else.</pre>\n\n</div></body></html>",
-        "noQuirksBodyHtml": "\n\n<b><nobr></nobr></b><div><b><nobr>This text is in a div inside a nobr</nobr>More text that should not be in the nobr, i.e., the\nnobr should have closed the div inside it implicitly. </b><pre>A pre tag outside everything else.</pre>\n\n</div>"
-      }
-    }
-  ],
-  "webkit01.dat": [
-    {
-      "data": "Test",
-      "errors": [
-        "(1,4): expected-doctype-but-got-chars"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "Test"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>Test</body></html>",
-        "noQuirksBodyHtml": "Test"
-      }
-    },
-    {
-      "data": "<div></div>",
-      "errors": [
-        "(1,5): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "div": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "div"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><div></div></body></html>",
-        "noQuirksBodyHtml": "<div></div>"
-      }
-    },
-    {
-      "data": "<div>Test</div>",
-      "errors": [
-        "(1,5): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "div": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "div",
-                    "children": [
-                      {
-                        "text": "Test"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><div>Test</div></body></html>",
-        "noQuirksBodyHtml": "<div>Test</div>"
-      }
-    },
-    {
-      "data": "<di",
-      "errors": [
-        "(1,3): eof-in-tag-name",
-        "(1,3): expected-doctype-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body></body></html>",
-        "noQuirksBodyHtml": ""
-      }
-    },
-    {
-      "data": "<div>Hello</div>\n<script>\nconsole.log(\"PASS\");\n</script>\n<div>Bye</div>",
-      "errors": [
-        "(1,5): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "div": true,
-            "script": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "div",
-                    "children": [
-                      {
-                        "text": "Hello"
-                      }
-                    ]
-                  },
-                  {
-                    "text": "\n"
-                  },
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "\nconsole.log(\"PASS\");\n",
-                        "no_escape": true
-                      }
-                    ]
-                  },
-                  {
-                    "text": "\n"
-                  },
-                  {
-                    "tag": "div",
-                    "children": [
-                      {
-                        "text": "Bye"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><div>Hello</div>\n<script>\nconsole.log(\"PASS\");\n</script>\n<div>Bye</div></body></html>",
-        "noQuirksBodyHtml": "<div>Hello</div>\n<script>\nconsole.log(\"PASS\");\n</script>\n<div>Bye</div>"
-      }
-    },
-    {
-      "data": "<div foo=\"bar\">Hello</div>",
-      "errors": [
-        "(1,15): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "div": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "div",
-                    "attrs": [
-                      {
-                        "name": "foo",
-                        "value": "bar"
-                      }
-                    ],
-                    "children": [
-                      {
-                        "text": "Hello"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><div foo=\"bar\">Hello</div></body></html>",
-        "noQuirksBodyHtml": "<div foo=\"bar\">Hello</div>"
-      }
-    },
-    {
-      "data": "<div>Hello</div>\n<script>\nconsole.log(\"FOO<span>BAR</span>BAZ\");\n</script>\n<div>Bye</div>",
-      "errors": [
-        "(1,5): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "div": true,
-            "script": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "div",
-                    "children": [
-                      {
-                        "text": "Hello"
-                      }
-                    ]
-                  },
-                  {
-                    "text": "\n"
-                  },
-                  {
-                    "tag": "script",
-                    "children": [
-                      {
-                        "text": "\nconsole.log(\"FOO<span>BAR</span>BAZ\");\n",
-                        "no_escape": true
-                      }
-                    ]
-                  },
-                  {
-                    "text": "\n"
-                  },
-                  {
-                    "tag": "div",
-                    "children": [
-                      {
-                        "text": "Bye"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><div>Hello</div>\n<script>\nconsole.log(\"FOO<span>BAR</span>BAZ\");\n</script>\n<div>Bye</div></body></html>",
-        "noQuirksBodyHtml": "<div>Hello</div>\n<script>\nconsole.log(\"FOO<span>BAR</span>BAZ\");\n</script>\n<div>Bye</div>"
-      }
-    },
-    {
-      "data": "<foo bar=\"baz\"></foo><potato quack=\"duck\"></potato>",
-      "errors": [
-        "(1,15): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "foo": true,
-            "potato": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "foo",
-                    "attrs": [
-                      {
-                        "name": "bar",
-                        "value": "baz"
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "potato",
-                    "attrs": [
-                      {
-                        "name": "quack",
-                        "value": "duck"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><foo bar=\"baz\"></foo><potato quack=\"duck\"></potato></body></html>",
-        "noQuirksBodyHtml": "<foo bar=\"baz\"></foo><potato quack=\"duck\"></potato>"
-      }
-    },
-    {
-      "data": "<foo bar=\"baz\"><potato quack=\"duck\"></potato></foo>",
-      "errors": [
-        "(1,15): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "foo": true,
-            "potato": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "foo",
-                    "attrs": [
-                      {
-                        "name": "bar",
-                        "value": "baz"
-                      }
-                    ],
-                    "children": [
-                      {
-                        "tag": "potato",
-                        "attrs": [
-                          {
-                            "name": "quack",
-                            "value": "duck"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><foo bar=\"baz\"><potato quack=\"duck\"></potato></foo></body></html>",
-        "noQuirksBodyHtml": "<foo bar=\"baz\"><potato quack=\"duck\"></potato></foo>"
-      }
-    },
-    {
-      "data": "<foo></foo bar=\"baz\"><potato></potato quack=\"duck\">",
-      "errors": [
-        "(1,5): expected-doctype-but-got-start-tag",
-        "(1,21): attributes-in-end-tag",
-        "(1,51): attributes-in-end-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "foo": true,
-            "potato": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "foo"
-                  },
-                  {
-                    "tag": "potato"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><foo></foo><potato></potato></body></html>",
-        "noQuirksBodyHtml": "<foo></foo><potato></potato>"
-      }
-    },
-    {
-      "data": "</ tttt>",
-      "errors": [
-        "(1,2): expected-closing-tag-but-got-char",
-        "(1,8): expected-doctype-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "comment": true
-        },
-        "tree": [
-          {
-            "comment": " tttt"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<!-- tttt--><html><head></head><body></body></html>",
-        "noQuirksBodyHtml": "<!-- tttt-->"
-      }
-    },
-    {
-      "data": "<div FOO ><img><img></div>",
-      "errors": [
-        "(1,10): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "div": true,
-            "img": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "div",
-                    "attrs": [
-                      {
-                        "name": "foo",
-                        "value": ""
-                      }
-                    ],
-                    "children": [
-                      {
-                        "tag": "img"
-                      },
-                      {
-                        "tag": "img"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><div foo=\"\"><img><img></div></body></html>",
-        "noQuirksBodyHtml": "<div foo=\"\"><img><img></div>"
-      }
-    },
-    {
-      "data": "<p>Test</p<p>Test2</p>",
-      "errors": [
-        "(1,3): expected-doctype-but-got-start-tag",
-        "(1,13): unexpected-end-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "p": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "p",
-                    "children": [
-                      {
-                        "text": "TestTest2"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><p>TestTest2</p></body></html>",
-        "noQuirksBodyHtml": "<p>TestTest2</p>"
-      }
-    },
-    {
-      "data": "<rdar://problem/6869687>",
-      "errors": [
-        "(1,7): unexpected-character-after-solidus-in-tag",
-        "(1,8): unexpected-character-after-solidus-in-tag",
-        "(1,16): unexpected-character-after-solidus-in-tag",
-        "(1,24): expected-doctype-but-got-start-tag",
-        "(1,24): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "rdar:": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "rdar:",
-                    "attrs": [
-                      {
-                        "name": "6869687",
-                        "value": ""
-                      },
-                      {
-                        "name": "problem",
-                        "value": ""
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><rdar: problem=\"\" 6869687=\"\"></rdar:></body></html>",
-        "noQuirksBodyHtml": "<rdar: problem=\"\" 6869687=\"\"></rdar:>"
-      }
-    },
-    {
-      "data": "<A>test< /A>",
-      "errors": [
-        "(1,3): expected-doctype-but-got-start-tag",
-        "(1,8): expected-tag-name",
-        "(1,12): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "a": true
-          },
-          "escaped": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "a",
-                    "children": [
-                      {
-                        "text": "test< /A>",
-                        "escaped": true
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><a>test&lt; /A&gt;</a></body></html>",
-        "noQuirksBodyHtml": "<a>test&lt; /A&gt;</a>"
-      }
-    },
-    {
-      "data": "&lt;",
-      "errors": [
-        "(1,4): expected-doctype-but-got-chars"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "escaped": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "<",
-                    "escaped": true
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>&lt;</body></html>",
-        "noQuirksBodyHtml": "&lt;"
-      }
-    },
-    {
-      "data": "<body foo='bar'><body foo='baz' yo='mama'>",
-      "errors": [
-        "(1,16): expected-doctype-but-got-start-tag",
-        "(1,42): unexpected-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "attrs": [
-                  {
-                    "name": "foo",
-                    "value": "bar"
-                  },
-                  {
-                    "name": "yo",
-                    "value": "mama"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body foo=\"bar\" yo=\"mama\"></body></html>",
-        "noQuirksBodyHtml": ""
-      }
-    },
-    {
-      "data": "<body></br foo=\"bar\"></body>",
-      "errors": [
-        "(1,6): expected-doctype-but-got-start-tag",
-        "(1,21): attributes-in-end-tag",
-        "(1,21): unexpected-end-tag-treated-as"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "br": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "br"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><br></body></html>",
-        "noQuirksBodyHtml": "<br>"
-      }
-    },
-    {
-      "data": "<bdy><br foo=\"bar\"></body>",
-      "errors": [
-        "(1,5): expected-doctype-but-got-start-tag",
-        "(1,26): expected-one-end-tag-but-got-another"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "bdy": true,
-            "br": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "bdy",
-                    "children": [
-                      {
-                        "tag": "br",
-                        "attrs": [
-                          {
-                            "name": "foo",
-                            "value": "bar"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><bdy><br foo=\"bar\"></bdy></body></html>",
-        "noQuirksBodyHtml": "<bdy><br foo=\"bar\"></bdy>"
-      }
-    },
-    {
-      "data": "<body></body></br foo=\"bar\">",
-      "errors": [
-        "(1,6): expected-doctype-but-got-start-tag",
-        "(1,28): attributes-in-end-tag",
-        "(1,28): unexpected-end-tag-after-body",
-        "(1,28): unexpected-end-tag-treated-as"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "br": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "br"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><br></body></html>",
-        "noQuirksBodyHtml": "<br>"
-      }
-    },
-    {
-      "data": "<bdy></body><br foo=\"bar\">",
-      "errors": [
-        "(1,5): expected-doctype-but-got-start-tag",
-        "(1,12): expected-one-end-tag-but-got-another",
-        "(1,26): unexpected-start-tag-after-body",
-        "(1,26): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "bdy": true,
-            "br": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "bdy",
-                    "children": [
-                      {
-                        "tag": "br",
-                        "attrs": [
-                          {
-                            "name": "foo",
-                            "value": "bar"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><bdy><br foo=\"bar\"></bdy></body></html>",
-        "noQuirksBodyHtml": "<bdy><br foo=\"bar\"></bdy>"
-      }
-    },
-    {
-      "data": "<html><body></body></html><!-- Hi there -->",
-      "errors": [
-        "(1,6): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "comment": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          },
-          {
-            "comment": " Hi there "
-          }
-        ],
-        "html": "<html><head></head><body></body></html><!-- Hi there -->",
-        "noQuirksBodyHtml": "<!-- Hi there -->"
-      }
-    },
-    {
-      "data": "<html><body></body></html>x<!-- Hi there -->",
-      "errors": [
-        "(1,6): expected-doctype-but-got-start-tag",
-        "(1,27): expected-eof-but-got-char"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "comment": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "x"
-                  },
-                  {
-                    "comment": " Hi there "
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>x<!-- Hi there --></body></html>",
-        "noQuirksBodyHtml": "x<!-- Hi there -->"
-      }
-    },
-    {
-      "data": "<html><body></body></html>x<!-- Hi there --></html><!-- Again -->",
-      "errors": [
-        "(1,6): expected-doctype-but-got-start-tag",
-        "(1,27): expected-eof-but-got-char"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "comment": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "x"
-                  },
-                  {
-                    "comment": " Hi there "
-                  }
-                ]
-              }
-            ]
-          },
-          {
-            "comment": " Again "
-          }
-        ],
-        "html": "<html><head></head><body>x<!-- Hi there --></body></html><!-- Again -->",
-        "noQuirksBodyHtml": "x<!-- Hi there --><!-- Again -->"
-      }
-    },
-    {
-      "data": "<html><body></body></html>x<!-- Hi there --></body></html><!-- Again -->",
-      "errors": [
-        "(1,6): expected-doctype-but-got-start-tag",
-        "(1,27): expected-eof-but-got-char"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          },
-          "comment": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "x"
-                  },
-                  {
-                    "comment": " Hi there "
-                  }
-                ]
-              }
-            ]
-          },
-          {
-            "comment": " Again "
-          }
-        ],
-        "html": "<html><head></head><body>x<!-- Hi there --></body></html><!-- Again -->",
-        "noQuirksBodyHtml": "x<!-- Hi there --><!-- Again -->"
-      }
-    },
-    {
-      "data": "<html><body><ruby><div><rp>xx</rp></div></ruby></body></html>",
-      "errors": [
-        "(1,6): expected-doctype-but-got-start-tag",
-        "(1,27): XXX-undefined-error"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "ruby": true,
-            "div": true,
-            "rp": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "ruby",
-                    "children": [
-                      {
-                        "tag": "div",
-                        "children": [
-                          {
-                            "tag": "rp",
-                            "children": [
-                              {
-                                "text": "xx"
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><ruby><div><rp>xx</rp></div></ruby></body></html>",
-        "noQuirksBodyHtml": "<ruby><div><rp>xx</rp></div></ruby>"
-      }
-    },
-    {
-      "data": "<html><body><ruby><div><rt>xx</rt></div></ruby></body></html>",
-      "errors": [
-        "(1,6): expected-doctype-but-got-start-tag",
-        "(1,27): XXX-undefined-error"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "ruby": true,
-            "div": true,
-            "rt": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "ruby",
-                    "children": [
-                      {
-                        "tag": "div",
-                        "children": [
-                          {
-                            "tag": "rt",
-                            "children": [
-                              {
-                                "text": "xx"
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><ruby><div><rt>xx</rt></div></ruby></body></html>",
-        "noQuirksBodyHtml": "<ruby><div><rt>xx</rt></div></ruby>"
-      }
-    },
-    {
-      "data": "<html><frameset><!--1--><noframes>A</noframes><!--2--></frameset><!--3--><noframes>B</noframes><!--4--></html><!--5--><noframes>C</noframes><!--6-->",
-      "errors": [
-        "(1,6): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "frameset": true,
-            "noframes": true
-          },
-          "comment": true,
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "frameset",
-                "children": [
-                  {
-                    "comment": "1"
-                  },
-                  {
-                    "tag": "noframes",
-                    "children": [
-                      {
-                        "text": "A",
-                        "no_escape": true
-                      }
-                    ]
-                  },
-                  {
-                    "comment": "2"
-                  }
-                ]
-              },
-              {
-                "comment": "3"
-              },
-              {
-                "tag": "noframes",
-                "children": [
-                  {
-                    "text": "B",
-                    "no_escape": true
-                  }
-                ]
-              },
-              {
-                "comment": "4"
-              },
-              {
-                "tag": "noframes",
-                "children": [
-                  {
-                    "text": "C",
-                    "no_escape": true
-                  }
-                ]
-              }
-            ]
-          },
-          {
-            "comment": "5"
-          },
-          {
-            "comment": "6"
-          }
-        ],
-        "html": "<html><head></head><frameset><!--1--><noframes>A</noframes><!--2--></frameset><!--3--><noframes>B</noframes><!--4--><noframes>C</noframes></html><!--5--><!--6-->",
-        "noQuirksBodyHtml": "<!--1--><noframes>A</noframes><!--2--><!--3--><noframes>B</noframes><!--4--><!--5--><noframes>C</noframes><!--6-->"
-      }
-    },
-    {
-      "data": "<select><option>A<select><option>B<select><option>C<select><option>D<select><option>E<select><option>F<select><option>G<select>",
-      "errors": [
-        "(1,8): expected-doctype-but-got-start-tag",
-        "(1,25): unexpected-select-in-select",
-        "(1,59): unexpected-select-in-select",
-        "(1,93): unexpected-select-in-select",
-        "(1,127): unexpected-select-in-select"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "select": true,
-            "option": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "select",
-                    "children": [
-                      {
-                        "tag": "option",
-                        "children": [
-                          {
-                            "text": "A"
-                          }
-                        ]
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "option",
-                    "children": [
-                      {
-                        "text": "B"
-                      },
-                      {
-                        "tag": "select",
-                        "children": [
-                          {
-                            "tag": "option",
-                            "children": [
-                              {
-                                "text": "C"
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "option",
-                    "children": [
-                      {
-                        "text": "D"
-                      },
-                      {
-                        "tag": "select",
-                        "children": [
-                          {
-                            "tag": "option",
-                            "children": [
-                              {
-                                "text": "E"
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "option",
-                    "children": [
-                      {
-                        "text": "F"
-                      },
-                      {
-                        "tag": "select",
-                        "children": [
-                          {
-                            "tag": "option",
-                            "children": [
-                              {
-                                "text": "G"
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><select><option>A</option></select><option>B<select><option>C</option></select></option><option>D<select><option>E</option></select></option><option>F<select><option>G</option></select></option></body></html>",
-        "noQuirksBodyHtml": "<select><option>A</option></select><option>B<select><option>C</option></select></option><option>D<select><option>E</option></select></option><option>F<select><option>G</option></select></option>"
-      }
-    },
-    {
-      "data": "<dd><dd><dt><dt><dd><li><li>",
-      "errors": [
-        "(1,4): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "dd": true,
-            "dt": true,
-            "li": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "dd"
-                  },
-                  {
-                    "tag": "dd"
-                  },
-                  {
-                    "tag": "dt"
-                  },
-                  {
-                    "tag": "dt"
-                  },
-                  {
-                    "tag": "dd",
-                    "children": [
-                      {
-                        "tag": "li"
-                      },
-                      {
-                        "tag": "li"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><dd></dd><dd></dd><dt></dt><dt></dt><dd><li></li><li></li></dd></body></html>",
-        "noQuirksBodyHtml": "<dd></dd><dd></dd><dt></dt><dt></dt><dd><li></li><li></li></dd>"
-      }
-    },
-    {
-      "data": "<div><b></div><div><nobr>a<nobr>",
-      "errors": [
-        "(1,5): expected-doctype-but-got-start-tag",
-        "(1,14): end-tag-too-early",
-        "(1,32): unexpected-start-tag-implies-end-tag",
-        "(1,32): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "div": true,
-            "b": true,
-            "nobr": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "div",
-                    "children": [
-                      {
-                        "tag": "b"
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "div",
-                    "children": [
-                      {
-                        "tag": "b",
-                        "children": [
-                          {
-                            "tag": "nobr",
-                            "children": [
-                              {
-                                "text": "a"
-                              }
-                            ]
-                          },
-                          {
-                            "tag": "nobr"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><div><b></b></div><div><b><nobr>a</nobr><nobr></nobr></b></div></body></html>",
-        "noQuirksBodyHtml": "<div><b></b></div><div><b><nobr>a</nobr><nobr></nobr></b></div>"
-      }
-    },
-    {
-      "data": "<head></head>\n<body></body>",
-      "errors": [
-        "(1,6): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "text": "\n"
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head>\n<body></body></html>",
-        "noQuirksBodyHtml": "\n"
-      }
-    },
-    {
-      "data": "<head></head> <style></style>ddd",
-      "errors": [
-        "(1,6): expected-doctype-but-got-start-tag",
-        "(1,21): unexpected-start-tag-out-of-my-head"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "style": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head",
-                "children": [
-                  {
-                    "tag": "style"
-                  }
-                ]
-              },
-              {
-                "text": " "
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "ddd"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head><style></style></head> <body>ddd</body></html>",
-        "noQuirksBodyHtml": " <style></style>ddd"
-      }
-    },
-    {
-      "data": "<kbd><table></kbd><col><select><tr>",
-      "errors": [
-        "(1,5): expected-doctype-but-got-start-tag",
-        "(1,18): unexpected-end-tag-implies-table-voodoo",
-        "(1,18): unexpected-end-tag",
-        "(1,31): unexpected-start-tag-implies-table-voodoo",
-        "(1,35): unexpected-table-element-start-tag-in-select-in-table",
-        "(1,35): eof-in-table"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "kbd": true,
-            "select": true,
-            "table": true,
-            "colgroup": true,
-            "col": true,
-            "tbody": true,
-            "tr": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "kbd",
-                    "children": [
-                      {
-                        "tag": "select"
-                      },
-                      {
-                        "tag": "table",
-                        "children": [
-                          {
-                            "tag": "colgroup",
-                            "children": [
-                              {
-                                "tag": "col"
-                              }
-                            ]
-                          },
-                          {
-                            "tag": "tbody",
-                            "children": [
-                              {
-                                "tag": "tr"
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><kbd><select></select><table><colgroup><col></colgroup><tbody><tr></tr></tbody></table></kbd></body></html>",
-        "noQuirksBodyHtml": "<kbd><select></select><table><colgroup><col></colgroup><tbody><tr></tr></tbody></table></kbd>"
-      }
-    },
-    {
-      "data": "<kbd><table></kbd><col><select><tr></table><div>",
-      "errors": [
-        "(1,5): expected-doctype-but-got-start-tag",
-        "(1,18): unexpected-end-tag-implies-table-voodoo",
-        "(1,18): unexpected-end-tag",
-        "(1,31): unexpected-start-tag-implies-table-voodoo",
-        "(1,35): unexpected-table-element-start-tag-in-select-in-table",
-        "(1,48): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "kbd": true,
-            "select": true,
-            "table": true,
-            "colgroup": true,
-            "col": true,
-            "tbody": true,
-            "tr": true,
-            "div": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "kbd",
-                    "children": [
-                      {
-                        "tag": "select"
-                      },
-                      {
-                        "tag": "table",
-                        "children": [
-                          {
-                            "tag": "colgroup",
-                            "children": [
-                              {
-                                "tag": "col"
-                              }
-                            ]
-                          },
-                          {
-                            "tag": "tbody",
-                            "children": [
-                              {
-                                "tag": "tr"
-                              }
-                            ]
-                          }
-                        ]
-                      },
-                      {
-                        "tag": "div"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><kbd><select></select><table><colgroup><col></colgroup><tbody><tr></tr></tbody></table><div></div></kbd></body></html>",
-        "noQuirksBodyHtml": "<kbd><select></select><table><colgroup><col></colgroup><tbody><tr></tr></tbody></table><div></div></kbd>"
-      }
-    },
-    {
-      "data": "<a><li><style></style><title></title></a>",
-      "errors": [
-        "(1,3): expected-doctype-but-got-start-tag",
-        "(1,41): adoption-agency-1.3"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "a": true,
-            "li": true,
-            "style": true,
-            "title": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "a"
-                  },
-                  {
-                    "tag": "li",
-                    "children": [
-                      {
-                        "tag": "a",
-                        "children": [
-                          {
-                            "tag": "style"
-                          },
-                          {
-                            "tag": "title"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><a></a><li><a><style></style><title></title></a></li></body></html>",
-        "noQuirksBodyHtml": "<a></a><li><a><style></style><title></title></a></li>"
-      }
-    },
-    {
-      "data": "<font></p><p><meta><title></title></font>",
-      "errors": [
-        "(1,6): expected-doctype-but-got-start-tag",
-        "(1,10): unexpected-end-tag",
-        "(1,41): adoption-agency-1.3"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "font": true,
-            "p": true,
-            "meta": true,
-            "title": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "font",
-                    "children": [
-                      {
-                        "tag": "p"
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "p",
-                    "children": [
-                      {
-                        "tag": "font",
-                        "children": [
-                          {
-                            "tag": "meta"
-                          },
-                          {
-                            "tag": "title"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><font><p></p></font><p><font><meta><title></title></font></p></body></html>",
-        "noQuirksBodyHtml": "<font><p></p></font><p><font><meta><title></title></font></p>"
-      }
-    },
-    {
-      "data": "<a><center><title></title><a>",
-      "errors": [
-        "(1,3): expected-doctype-but-got-start-tag",
-        "(1,29): unexpected-start-tag-implies-end-tag",
-        "(1,29): adoption-agency-1.3",
-        "(1,29): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "a": true,
-            "center": true,
-            "title": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "a"
-                  },
-                  {
-                    "tag": "center",
-                    "children": [
-                      {
-                        "tag": "a",
-                        "children": [
-                          {
-                            "tag": "title"
-                          }
-                        ]
-                      },
-                      {
-                        "tag": "a"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><a></a><center><a><title></title></a><a></a></center></body></html>",
-        "noQuirksBodyHtml": "<a></a><center><a><title></title></a><a></a></center>"
-      }
-    },
-    {
-      "data": "<svg><title><div>",
-      "errors": [
-        "(1,5): expected-doctype-but-got-start-tag",
-        "(1,17): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "svg svg": true,
-            "svg title": true,
-            "div": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "svg",
-                    "ns": "http://www.w3.org/2000/svg",
-                    "children": [
-                      {
-                        "tag": "title",
-                        "ns": "http://www.w3.org/2000/svg",
-                        "children": [
-                          {
-                            "tag": "div"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><svg><title><div></div></title></svg></body></html>",
-        "noQuirksBodyHtml": "<svg><title><div></div></title></svg>"
-      }
-    },
-    {
-      "data": "<svg><title><rect><div>",
-      "errors": [
-        "(1,5): expected-doctype-but-got-start-tag",
-        "(1,23): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "svg svg": true,
-            "svg title": true,
-            "rect": true,
-            "div": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "svg",
-                    "ns": "http://www.w3.org/2000/svg",
-                    "children": [
-                      {
-                        "tag": "title",
-                        "ns": "http://www.w3.org/2000/svg",
-                        "children": [
-                          {
-                            "tag": "rect",
-                            "children": [
-                              {
-                                "tag": "div"
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><svg><title><rect><div></div></rect></title></svg></body></html>",
-        "noQuirksBodyHtml": "<svg><title><rect><div></div></rect></title></svg>"
-      }
-    },
-    {
-      "data": "<svg><title><svg><div>",
-      "errors": [
-        "(1,5): expected-doctype-but-got-start-tag",
-        "(1,22): unexpected-html-element-in-foreign-content",
-        "(1,22): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "svg svg": true,
-            "svg title": true,
-            "div": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "svg",
-                    "ns": "http://www.w3.org/2000/svg",
-                    "children": [
-                      {
-                        "tag": "title",
-                        "ns": "http://www.w3.org/2000/svg",
-                        "children": [
-                          {
-                            "tag": "svg",
-                            "ns": "http://www.w3.org/2000/svg"
-                          },
-                          {
-                            "tag": "div"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><svg><title><svg></svg><div></div></title></svg></body></html>",
-        "noQuirksBodyHtml": "<svg><title><svg><div></div></svg></title></svg>"
-      }
-    },
-    {
-      "data": "<img <=\"\" FAIL>",
-      "errors": [
-        "(1,6): invalid-character-in-attribute-name",
-        "(1,15): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "img": true
-          },
-          "attrWithFunnyChar": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "img",
-                    "attrs": [
-                      {
-                        "name": "<",
-                        "value": ""
-                      },
-                      {
-                        "name": "fail",
-                        "value": ""
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><img <=\"\" fail=\"\"></body></html>",
-        "noQuirksBodyHtml": "<img <=\"\" fail=\"\">"
-      }
-    },
-    {
-      "data": "<ul><li><div id='foo'/>A</li><li>B<div>C</div></li></ul>",
-      "errors": [
-        "(1,4): expected-doctype-but-got-start-tag",
-        "(1,23): non-void-element-with-trailing-solidus",
-        "(1,29): end-tag-too-early"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "ul": true,
-            "li": true,
-            "div": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "ul",
-                    "children": [
-                      {
-                        "tag": "li",
-                        "children": [
-                          {
-                            "tag": "div",
-                            "attrs": [
-                              {
-                                "name": "id",
-                                "value": "foo"
-                              }
-                            ],
-                            "children": [
-                              {
-                                "text": "A"
-                              }
-                            ]
-                          }
-                        ]
-                      },
-                      {
-                        "tag": "li",
-                        "children": [
-                          {
-                            "text": "B"
-                          },
-                          {
-                            "tag": "div",
-                            "children": [
-                              {
-                                "text": "C"
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><ul><li><div id=\"foo\">A</div></li><li>B<div>C</div></li></ul></body></html>",
-        "noQuirksBodyHtml": "<ul><li><div id=\"foo\">A</div></li><li>B<div>C</div></li></ul>"
-      }
-    },
-    {
-      "data": "<svg><em><desc></em>",
-      "errors": [
-        "(1,5): expected-doctype-but-got-start-tag",
-        "(1,9): unexpected-html-element-in-foreign-content",
-        "(1,20): adoption-agency-1.3"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "svg svg": true,
-            "em": true,
-            "desc": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "svg",
-                    "ns": "http://www.w3.org/2000/svg"
-                  },
-                  {
-                    "tag": "em",
-                    "children": [
-                      {
-                        "tag": "desc"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><svg></svg><em><desc></desc></em></body></html>",
-        "noQuirksBodyHtml": "<svg><em><desc></desc></em></svg>"
-      }
-    },
-    {
-      "data": "<table><tr><td><svg><desc><td></desc><circle>",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true,
-            "tbody": true,
-            "tr": true,
-            "td": true,
-            "svg svg": true,
-            "svg desc": true,
-            "circle": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "tbody",
-                        "children": [
-                          {
-                            "tag": "tr",
-                            "children": [
-                              {
-                                "tag": "td",
-                                "children": [
-                                  {
-                                    "tag": "svg",
-                                    "ns": "http://www.w3.org/2000/svg",
-                                    "children": [
-                                      {
-                                        "tag": "desc",
-                                        "ns": "http://www.w3.org/2000/svg"
-                                      }
-                                    ]
-                                  }
-                                ]
-                              },
-                              {
-                                "tag": "td",
-                                "children": [
-                                  {
-                                    "tag": "circle"
-                                  }
-                                ]
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><table><tbody><tr><td><svg><desc></desc></svg></td><td><circle></circle></td></tr></tbody></table></body></html>",
-        "noQuirksBodyHtml": "<table><tbody><tr><td><svg><desc></desc></svg></td><td><circle></circle></td></tr></tbody></table>"
-      }
-    },
-    {
-      "data": "<svg><tfoot></mi><td>",
-      "errors": [
-        "(1,5): expected-doctype-but-got-start-tag",
-        "(1,17): unexpected-end-tag",
-        "(1,17): unexpected-end-tag",
-        "(1,21): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "svg svg": true,
-            "svg tfoot": true,
-            "svg td": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "svg",
-                    "ns": "http://www.w3.org/2000/svg",
-                    "children": [
-                      {
-                        "tag": "tfoot",
-                        "ns": "http://www.w3.org/2000/svg",
-                        "children": [
-                          {
-                            "tag": "td",
-                            "ns": "http://www.w3.org/2000/svg"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><svg><tfoot><td></td></tfoot></svg></body></html>",
-        "noQuirksBodyHtml": "<svg><tfoot><td></td></tfoot></svg>"
-      }
-    },
-    {
-      "data": "<math><mrow><mrow><mn>1</mn></mrow><mi>a</mi></mrow></math>",
-      "errors": [
-        "(1,6): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "math math": true,
-            "math mrow": true,
-            "math mn": true,
-            "math mi": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "math",
-                    "ns": "http://www.w3.org/1998/Math/MathML",
-                    "children": [
-                      {
-                        "tag": "mrow",
-                        "ns": "http://www.w3.org/1998/Math/MathML",
-                        "children": [
-                          {
-                            "tag": "mrow",
-                            "ns": "http://www.w3.org/1998/Math/MathML",
-                            "children": [
-                              {
-                                "tag": "mn",
-                                "ns": "http://www.w3.org/1998/Math/MathML",
-                                "children": [
-                                  {
-                                    "text": "1"
-                                  }
-                                ]
-                              }
-                            ]
-                          },
-                          {
-                            "tag": "mi",
-                            "ns": "http://www.w3.org/1998/Math/MathML",
-                            "children": [
-                              {
-                                "text": "a"
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><math><mrow><mrow><mn>1</mn></mrow><mi>a</mi></mrow></math></body></html>",
-        "noQuirksBodyHtml": "<math><mrow><mrow><mn>1</mn></mrow><mi>a</mi></mrow></math>"
-      }
-    },
-    {
-      "data": "<!doctype html><input type=\"hidden\"><frameset>",
-      "errors": [
-        "(1,46): unexpected-start-tag",
-        "(1,46): eof-in-frameset"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "frameset": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "frameset"
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><frameset></frameset></html>",
-        "noQuirksBodyHtml": "<input type=\"hidden\">"
-      }
-    },
-    {
-      "data": "<!doctype html><input type=\"button\"><frameset>",
-      "errors": [
-        "(1,46): unexpected-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "input": true
-          },
-          "doctype": true
-        },
-        "tree": [
-          {
-            "doctype": "html"
-          },
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "input",
-                    "attrs": [
-                      {
-                        "name": "type",
-                        "value": "button"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<!DOCTYPE html><html><head></head><body><input type=\"button\"></body></html>",
-        "noQuirksBodyHtml": "<input type=\"button\">"
-      }
-    }
-  ],
-  "webkit02.dat": [
-    {
-      "data": "<foo bar=qux/>",
-      "errors": [
-        "(1,14): expected-doctype-but-got-start-tag",
-        "(1,14): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "foo": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "foo",
-                    "attrs": [
-                      {
-                        "name": "bar",
-                        "value": "qux/"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><foo bar=\"qux/\"></foo></body></html>",
-        "noQuirksBodyHtml": "<foo bar=\"qux/\"></foo>"
-      }
-    },
-    {
-      "data": "<p id=\"status\"><noscript><strong>A</strong></noscript><span>B</span></p>",
-      "errors": [
-        "(1,15): expected-doctype-but-got-start-tag"
-      ],
-      "script": "on",
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "p": true,
-            "noscript": true,
-            "span": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "p",
-                    "attrs": [
-                      {
-                        "name": "id",
-                        "value": "status"
-                      }
-                    ],
-                    "children": [
-                      {
-                        "tag": "noscript",
-                        "children": [
-                          {
-                            "text": "<strong>A</strong>",
-                            "no_escape": true
-                          }
-                        ]
-                      },
-                      {
-                        "tag": "span",
-                        "children": [
-                          {
-                            "text": "B"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><p id=\"status\"><noscript><strong>A</strong></noscript><span>B</span></p></body></html>",
-        "noQuirksBodyHtml": "<p id=\"status\"><noscript><strong>A</strong></noscript><span>B</span></p>"
-      }
-    },
-    {
-      "data": "<p id=\"status\"><noscript><strong>A</strong></noscript><span>B</span></p>",
-      "errors": [
-        "(1,15): expected-doctype-but-got-start-tag"
-      ],
-      "script": "off",
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "p": true,
-            "noscript": true,
-            "strong": true,
-            "span": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "p",
-                    "attrs": [
-                      {
-                        "name": "id",
-                        "value": "status"
-                      }
-                    ],
-                    "children": [
-                      {
-                        "tag": "noscript",
-                        "children": [
-                          {
-                            "tag": "strong",
-                            "children": [
-                              {
-                                "text": "A"
-                              }
-                            ]
-                          }
-                        ]
-                      },
-                      {
-                        "tag": "span",
-                        "children": [
-                          {
-                            "text": "B"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><p id=\"status\"><noscript><strong>A</strong></noscript><span>B</span></p></body></html>",
-        "noQuirksBodyHtml": "<p id=\"status\"><noscript><strong>A</strong></noscript><span>B</span></p>"
-      }
-    },
-    {
-      "data": "<div><sarcasm><div></div></sarcasm></div>",
-      "errors": [
-        "(1,5): expected-doctype-but-got-start-tag"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "div": true,
-            "sarcasm": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "div",
-                    "children": [
-                      {
-                        "tag": "sarcasm",
-                        "children": [
-                          {
-                            "tag": "div"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><div><sarcasm><div></div></sarcasm></div></body></html>",
-        "noQuirksBodyHtml": "<div><sarcasm><div></div></sarcasm></div>"
-      }
-    },
-    {
-      "data": "<html><body><img src=\"\" border=\"0\" alt=\"><div>A</div></body></html>",
-      "errors": [
-        "(1,6): expected-doctype-but-got-start-tag",
-        "(1,67): eof-in-attribute-value-double-quote"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body"
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body></body></html>",
-        "noQuirksBodyHtml": ""
-      }
-    },
-    {
-      "data": "<table><td></tbody>A",
-      "errors": [
-        "(1,7): expected-doctype-but-got-start-tag",
-        "(1,11): unexpected-cell-in-table-body",
-        "(1,20): foster-parenting-character",
-        "(1,20): eof-in-table"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true,
-            "tbody": true,
-            "tr": true,
-            "td": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "text": "A"
-                  },
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "tbody",
-                        "children": [
-                          {
-                            "tag": "tr",
-                            "children": [
-                              {
-                                "tag": "td"
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body>A<table><tbody><tr><td></td></tr></tbody></table></body></html>",
-        "noQuirksBodyHtml": "A<table><tbody><tr><td></td></tr></tbody></table>"
-      }
-    },
-    {
-      "data": "<table><td></thead>A",
-      "errors": [
-        "(1,7): expected-doctype-but-got-start-tag",
-        "(1,11): unexpected-cell-in-table-body",
-        "(1,19): XXX-undefined-error",
-        "(1,20): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true,
-            "tbody": true,
-            "tr": true,
-            "td": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "tbody",
-                        "children": [
-                          {
-                            "tag": "tr",
-                            "children": [
-                              {
-                                "tag": "td",
-                                "children": [
-                                  {
-                                    "text": "A"
-                                  }
-                                ]
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><table><tbody><tr><td>A</td></tr></tbody></table></body></html>",
-        "noQuirksBodyHtml": "<table><tbody><tr><td>A</td></tr></tbody></table>"
-      }
-    },
-    {
-      "data": "<table><td></tfoot>A",
-      "errors": [
-        "(1,7): expected-doctype-but-got-start-tag",
-        "(1,11): unexpected-cell-in-table-body",
-        "(1,19): XXX-undefined-error",
-        "(1,20): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true,
-            "tbody": true,
-            "tr": true,
-            "td": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "tbody",
-                        "children": [
-                          {
-                            "tag": "tr",
-                            "children": [
-                              {
-                                "tag": "td",
-                                "children": [
-                                  {
-                                    "text": "A"
-                                  }
-                                ]
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><table><tbody><tr><td>A</td></tr></tbody></table></body></html>",
-        "noQuirksBodyHtml": "<table><tbody><tr><td>A</td></tr></tbody></table>"
-      }
-    },
-    {
-      "data": "<table><thead><td></tbody>A",
-      "errors": [
-        "(1,7): expected-doctype-but-got-start-tag",
-        "(1,18): unexpected-cell-in-table-body",
-        "(1,26): XXX-undefined-error",
-        "(1,27): expected-closing-tag-but-got-eof"
-      ],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "table": true,
-            "thead": true,
-            "tr": true,
-            "td": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "table",
-                    "children": [
-                      {
-                        "tag": "thead",
-                        "children": [
-                          {
-                            "tag": "tr",
-                            "children": [
-                              {
-                                "tag": "td",
-                                "children": [
-                                  {
-                                    "text": "A"
-                                  }
-                                ]
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><table><thead><tr><td>A</td></tr></thead></table></body></html>",
-        "noQuirksBodyHtml": "<table><thead><tr><td>A</td></tr></thead></table>"
-      }
-    },
-    {
-      "data": "<legend>test</legend>",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "legend": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "legend",
-                    "children": [
-                      {
-                        "text": "test"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><legend>test</legend></body></html>",
-        "noQuirksBodyHtml": "<legend>test</legend>"
-      }
-    },
-    {
-      "data": "<table><input>",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "input": true,
-            "table": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "input"
-                  },
-                  {
-                    "tag": "table"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><input><table></table></body></html>",
-        "noQuirksBodyHtml": "<input><table></table>"
-      }
-    },
-    {
-      "data": "<b><em><foo><foo><aside></b>",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "b": true,
-            "em": true,
-            "foo": true,
-            "aside": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "b",
-                    "children": [
-                      {
-                        "tag": "em",
-                        "children": [
-                          {
-                            "tag": "foo",
-                            "children": [
-                              {
-                                "tag": "foo"
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "em",
-                    "children": [
-                      {
-                        "tag": "aside",
-                        "children": [
-                          {
-                            "tag": "b"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><b><em><foo><foo></foo></foo></em></b><em><aside><b></b></aside></em></body></html>",
-        "noQuirksBodyHtml": "<b><em><foo><foo></foo></foo></em></b><em><aside><b></b></aside></em>"
-      }
-    },
-    {
-      "data": "<b><em><foo><foo><aside></b></em>",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "b": true,
-            "em": true,
-            "foo": true,
-            "aside": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "b",
-                    "children": [
-                      {
-                        "tag": "em",
-                        "children": [
-                          {
-                            "tag": "foo",
-                            "children": [
-                              {
-                                "tag": "foo"
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "em"
-                  },
-                  {
-                    "tag": "aside",
-                    "children": [
-                      {
-                        "tag": "em",
-                        "children": [
-                          {
-                            "tag": "b"
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><b><em><foo><foo></foo></foo></em></b><em></em><aside><em><b></b></em></aside></body></html>",
-        "noQuirksBodyHtml": "<b><em><foo><foo></foo></foo></em></b><em></em><aside><em><b></b></em></aside>"
-      }
-    },
-    {
-      "data": "<b><em><foo><foo><foo><aside></b>",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "b": true,
-            "em": true,
-            "foo": true,
-            "aside": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "b",
-                    "children": [
-                      {
-                        "tag": "em",
-                        "children": [
-                          {
-                            "tag": "foo",
-                            "children": [
-                              {
-                                "tag": "foo",
-                                "children": [
-                                  {
-                                    "tag": "foo"
-                                  }
-                                ]
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "aside",
-                    "children": [
-                      {
-                        "tag": "b"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><b><em><foo><foo><foo></foo></foo></foo></em></b><aside><b></b></aside></body></html>",
-        "noQuirksBodyHtml": "<b><em><foo><foo><foo></foo></foo></foo></em></b><aside><b></b></aside>"
-      }
-    },
-    {
-      "data": "<b><em><foo><foo><foo><aside></b></em>",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "b": true,
-            "em": true,
-            "foo": true,
-            "aside": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "b",
-                    "children": [
-                      {
-                        "tag": "em",
-                        "children": [
-                          {
-                            "tag": "foo",
-                            "children": [
-                              {
-                                "tag": "foo",
-                                "children": [
-                                  {
-                                    "tag": "foo"
-                                  }
-                                ]
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  },
-                  {
-                    "tag": "aside",
-                    "children": [
-                      {
-                        "tag": "b"
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><b><em><foo><foo><foo></foo></foo></foo></em></b><aside><b></b></aside></body></html>",
-        "noQuirksBodyHtml": "<b><em><foo><foo><foo></foo></foo></foo></em></b><aside><b></b></aside>"
-      }
-    },
-    {
-      "data": "<b><em><foo><foo><foo><foo><foo><foo><foo><foo><foo><foo><aside></b></em>",
-      "errors": [],
-      "fragment": {
-        "name": "div"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "b": true,
-            "em": true,
-            "foo": true,
-            "aside": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "b",
-            "children": [
-              {
-                "tag": "em",
-                "children": [
-                  {
-                    "tag": "foo",
-                    "children": [
-                      {
-                        "tag": "foo",
-                        "children": [
-                          {
-                            "tag": "foo",
-                            "children": [
-                              {
-                                "tag": "foo",
-                                "children": [
-                                  {
-                                    "tag": "foo",
-                                    "children": [
-                                      {
-                                        "tag": "foo",
-                                        "children": [
-                                          {
-                                            "tag": "foo",
-                                            "children": [
-                                              {
-                                                "tag": "foo",
-                                                "children": [
-                                                  {
-                                                    "tag": "foo",
-                                                    "children": [
-                                                      {
-                                                        "tag": "foo"
-                                                      }
-                                                    ]
-                                                  }
-                                                ]
-                                              }
-                                            ]
-                                          }
-                                        ]
-                                      }
-                                    ]
-                                  }
-                                ]
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          },
-          {
-            "tag": "aside",
-            "children": [
-              {
-                "tag": "b"
-              }
-            ]
-          }
-        ],
-        "html": "<b><em><foo><foo><foo><foo><foo><foo><foo><foo><foo><foo></foo></foo></foo></foo></foo></foo></foo></foo></foo></foo></em></b><aside><b></b></aside>",
-        "noQuirksBodyHtml": "<b><em><foo><foo><foo><foo><foo><foo><foo><foo><foo><foo></foo></foo></foo></foo></foo></foo></foo></foo></foo></foo></em></b><aside><b></b></aside>"
-      }
-    },
-    {
-      "data": "<b><em><foo><foob><foob><foob><foob><fooc><fooc><fooc><fooc><food><aside></b></em>",
-      "errors": [],
-      "fragment": {
-        "name": "div"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "b": true,
-            "em": true,
-            "foo": true,
-            "foob": true,
-            "fooc": true,
-            "food": true,
-            "aside": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "b",
-            "children": [
-              {
-                "tag": "em",
-                "children": [
-                  {
-                    "tag": "foo",
-                    "children": [
-                      {
-                        "tag": "foob",
-                        "children": [
-                          {
-                            "tag": "foob",
-                            "children": [
-                              {
-                                "tag": "foob",
-                                "children": [
-                                  {
-                                    "tag": "foob",
-                                    "children": [
-                                      {
-                                        "tag": "fooc",
-                                        "children": [
-                                          {
-                                            "tag": "fooc",
-                                            "children": [
-                                              {
-                                                "tag": "fooc",
-                                                "children": [
-                                                  {
-                                                    "tag": "fooc",
-                                                    "children": [
-                                                      {
-                                                        "tag": "food"
-                                                      }
-                                                    ]
-                                                  }
-                                                ]
-                                              }
-                                            ]
-                                          }
-                                        ]
-                                      }
-                                    ]
-                                  }
-                                ]
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          },
-          {
-            "tag": "aside",
-            "children": [
-              {
-                "tag": "b"
-              }
-            ]
-          }
-        ],
-        "html": "<b><em><foo><foob><foob><foob><foob><fooc><fooc><fooc><fooc><food></food></fooc></fooc></fooc></fooc></foob></foob></foob></foob></foo></em></b><aside><b></b></aside>",
-        "noQuirksBodyHtml": "<b><em><foo><foob><foob><foob><foob><fooc><fooc><fooc><fooc><food></food></fooc></fooc></fooc></fooc></foob></foob></foob></foob></foo></em></b><aside><b></b></aside>"
-      }
-    },
-    {
-      "data": "<option><XH<optgroup></optgroup>",
-      "errors": [],
-      "fragment": {
-        "name": "select"
-      },
-      "document": {
-        "props": {
-          "tags": {
-            "option": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "option"
-          }
-        ],
-        "html": "<option></option>",
-        "noQuirksBodyHtml": "<option><xh<optgroup></xh<optgroup></option>"
-      }
-    },
-    {
-      "data": "<svg><foreignObject><div>foo</div><plaintext></foreignObject></svg><div>bar</div>",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "svg svg": true,
-            "svg foreignObject": true,
-            "div": true,
-            "plaintext": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "svg",
-                    "ns": "http://www.w3.org/2000/svg",
-                    "children": [
-                      {
-                        "tag": "foreignObject",
-                        "ns": "http://www.w3.org/2000/svg",
-                        "children": [
-                          {
-                            "tag": "div",
-                            "children": [
-                              {
-                                "text": "foo"
-                              }
-                            ]
-                          },
-                          {
-                            "tag": "plaintext",
-                            "children": [
-                              {
-                                "text": "</foreignObject></svg><div>bar</div>",
-                                "no_escape": true
-                              }
-                            ]
-                          }
-                        ]
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><svg><foreignObject><div>foo</div><plaintext></foreignObject></svg><div>bar</div></plaintext></foreignObject></svg></body></html>",
-        "noQuirksBodyHtml": "<svg><foreignObject><div>foo</div><plaintext></foreignObject></svg><div>bar</div></plaintext></foreignObject></svg>"
-      }
-    },
-    {
-      "data": "<svg><foreignObject></foreignObject><title></svg>foo",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "svg svg": true,
-            "svg foreignObject": true,
-            "svg title": true
-          }
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "svg",
-                    "ns": "http://www.w3.org/2000/svg",
-                    "children": [
-                      {
-                        "tag": "foreignObject",
-                        "ns": "http://www.w3.org/2000/svg"
-                      },
-                      {
-                        "tag": "title",
-                        "ns": "http://www.w3.org/2000/svg"
-                      }
-                    ]
-                  },
-                  {
-                    "text": "foo"
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><svg><foreignObject></foreignObject><title></title></svg>foo</body></html>",
-        "noQuirksBodyHtml": "<svg><foreignObject></foreignObject><title></title></svg>foo"
-      }
-    },
-    {
-      "data": "</foreignObject><plaintext><div>foo</div>",
-      "errors": [],
-      "document": {
-        "props": {
-          "tags": {
-            "html": true,
-            "head": true,
-            "body": true,
-            "plaintext": true
-          },
-          "no_escape": true
-        },
-        "tree": [
-          {
-            "tag": "html",
-            "children": [
-              {
-                "tag": "head"
-              },
-              {
-                "tag": "body",
-                "children": [
-                  {
-                    "tag": "plaintext",
-                    "children": [
-                      {
-                        "text": "<div>foo</div>",
-                        "no_escape": true
-                      }
-                    ]
-                  }
-                ]
-              }
-            ]
-          }
-        ],
-        "html": "<html><head></head><body><plaintext><div>foo</div></plaintext></body></html>",
-        "noQuirksBodyHtml": "<plaintext><div>foo</div></plaintext>"
-      }
-    }
-  ]
-}
\ No newline at end of file
diff --git a/tests/phpunit/includes/title/ForeignTitleTest.php b/tests/phpunit/includes/title/ForeignTitleTest.php
deleted file mode 100644 (file)
index f2fccc7..0000000
+++ /dev/null
@@ -1,103 +0,0 @@
-<?php
-/**
- * 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; either version 2 of the License, or
- * (at your option) any later version.
- *
- * 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.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- * @author This, that and the other
- */
-
-/**
- * @covers ForeignTitle
- *
- * @group Title
- */
-class ForeignTitleTest extends MediaWikiTestCase {
-
-       public function basicProvider() {
-               return [
-                       [
-                               new ForeignTitle( 20, 'Contributor', 'JohnDoe' ),
-                               20, 'Contributor', 'JohnDoe'
-                       ],
-                       [
-                               new ForeignTitle( '1', 'Discussion', 'Capital' ),
-                               1, 'Discussion', 'Capital'
-                       ],
-                       [
-                               new ForeignTitle( 0, '', 'MainNamespace' ),
-                               0, '', 'MainNamespace'
-                       ],
-                       [
-                               new ForeignTitle( 4, 'Some ns', 'Article title with spaces' ),
-                               4, 'Some_ns', 'Article_title_with_spaces'
-                       ],
-               ];
-       }
-
-       /**
-        * @dataProvider basicProvider
-        */
-       public function testBasic( ForeignTitle $title, $expectedId, $expectedName,
-               $expectedText
-       ) {
-               $this->assertEquals( true, $title->isNamespaceIdKnown() );
-               $this->assertEquals( $expectedId, $title->getNamespaceId() );
-               $this->assertEquals( $expectedName, $title->getNamespaceName() );
-               $this->assertEquals( $expectedText, $title->getText() );
-       }
-
-       public function testUnknownNamespaceCheck() {
-               $title = new ForeignTitle( null, 'this', 'that' );
-
-               $this->assertEquals( false, $title->isNamespaceIdKnown() );
-               $this->assertEquals( 'this', $title->getNamespaceName() );
-               $this->assertEquals( 'that', $title->getText() );
-       }
-
-       public function testUnknownNamespaceError() {
-               $this->setExpectedException( MWException::class );
-               $title = new ForeignTitle( null, 'this', 'that' );
-               $title->getNamespaceId();
-       }
-
-       public function fullTextProvider() {
-               return [
-                       [
-                               new ForeignTitle( 20, 'Contributor', 'JohnDoe' ),
-                               'Contributor:JohnDoe'
-                       ],
-                       [
-                               new ForeignTitle( '1', 'Discussion', 'Capital' ),
-                               'Discussion:Capital'
-                       ],
-                       [
-                               new ForeignTitle( 0, '', 'MainNamespace' ),
-                               'MainNamespace'
-                       ],
-                       [
-                               new ForeignTitle( 4, 'Some ns', 'Article title with spaces' ),
-                               'Some_ns:Article_title_with_spaces'
-                       ],
-               ];
-       }
-
-       /**
-        * @dataProvider fullTextProvider
-        */
-       public function testFullText( ForeignTitle $title, $fullText ) {
-               $this->assertEquals( $fullText, $title->getFullText() );
-       }
-}
diff --git a/tests/phpunit/includes/title/NaiveForeignTitleFactoryTest.php b/tests/phpunit/includes/title/NaiveForeignTitleFactoryTest.php
deleted file mode 100644 (file)
index b8cc39f..0000000
+++ /dev/null
@@ -1,91 +0,0 @@
-<?php
-/**
- * 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; either version 2 of the License, or
- * (at your option) any later version.
- *
- * 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.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- * @author This, that and the other
- */
-
-/**
- * @covers NaiveForeignTitleFactory
- *
- * @group Title
- */
-class NaiveForeignTitleFactoryTest extends MediaWikiTestCase {
-
-       public function basicProvider() {
-               return [
-                       [
-                               'MainNamespaceArticle', 0,
-                               new ForeignTitle( 0, '', 'MainNamespaceArticle' ),
-                       ],
-                       [
-                               'MainNamespaceArticle', null,
-                               new ForeignTitle( null, '', 'MainNamespaceArticle' ),
-                       ],
-                       [
-                               'Talk:Nice_talk', 1,
-                               new ForeignTitle( 1, 'Talk', 'Nice_talk' ),
-                       ],
-                       [
-                               'Bogus:Nice_talk', 0,
-                               new ForeignTitle( 0, '', 'Bogus:Nice_talk' ),
-                       ],
-                       [
-                               'Bogus:Nice_talk', 9000, // non-existent local namespace ID
-                               new ForeignTitle( 9000, 'Bogus', 'Nice_talk' ),
-                       ],
-                       [
-                               'Bogus:Nice_talk', 4, // existing local namespace ID
-                               new ForeignTitle( 4, 'Bogus', 'Nice_talk' ),
-                       ],
-                       [
-                               'Talk:Extra:Nice_talk', 1,
-                               new ForeignTitle( 1, 'Talk', 'Extra:Nice_talk' ),
-                       ],
-                       [
-                               'Talk:Extra:Nice_talk', null,
-                               new ForeignTitle( null, 'Talk', 'Extra:Nice_talk' ),
-                       ],
-               ];
-       }
-
-       /**
-        * @dataProvider basicProvider
-        */
-       public function testBasic( $title, $ns, ForeignTitle $foreignTitle ) {
-               $factory = new NaiveForeignTitleFactory();
-               $testTitle = $factory->createForeignTitle( $title, $ns );
-
-               $this->assertEquals( $testTitle->isNamespaceIdKnown(),
-                       $foreignTitle->isNamespaceIdKnown() );
-
-               if (
-                       $testTitle->isNamespaceIdKnown() &&
-                       $foreignTitle->isNamespaceIdKnown()
-               ) {
-                       $this->assertEquals( $testTitle->getNamespaceId(),
-                               $foreignTitle->getNamespaceId() );
-               }
-
-               $this->assertEquals( $testTitle->getNamespaceName(),
-                       $foreignTitle->getNamespaceName() );
-               $this->assertEquals( $testTitle->getText(), $foreignTitle->getText() );
-
-               $this->assertEquals( str_replace( ' ', '_', $title ),
-                       $foreignTitle->getFullText() );
-       }
-}
diff --git a/tests/phpunit/includes/title/NamespaceAwareForeignTitleFactoryTest.php b/tests/phpunit/includes/title/NamespaceAwareForeignTitleFactoryTest.php
deleted file mode 100644 (file)
index 9aa3578..0000000
+++ /dev/null
@@ -1,101 +0,0 @@
-<?php
-/**
- * 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; either version 2 of the License, or
- * (at your option) any later version.
- *
- * 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.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- * @author This, that and the other
- */
-
-/**
- * @covers NamespaceAwareForeignTitleFactory
- *
- * @group Title
- */
-class NamespaceAwareForeignTitleFactoryTest extends MediaWikiTestCase {
-
-       public function basicProvider() {
-               return [
-                       [
-                               'MainNamespaceArticle', 0,
-                               new ForeignTitle( 0, '', 'MainNamespaceArticle' ),
-                       ],
-                       [
-                               'MainNamespaceArticle', null,
-                               new ForeignTitle( 0, '', 'MainNamespaceArticle' ),
-                       ],
-                       [
-                               'Magic:_The_Gathering', 0,
-                               new ForeignTitle( 0, '', 'Magic:_The_Gathering' ),
-                       ],
-                       [
-                               'Talk:Nice_talk', 1,
-                               new ForeignTitle( 1, 'Talk', 'Nice_talk' ),
-                       ],
-                       [
-                               'Talk:Magic:_The_Gathering', 1,
-                               new ForeignTitle( 1, 'Talk', 'Magic:_The_Gathering' ),
-                       ],
-                       [
-                               'Bogus:Nice_talk', 0,
-                               new ForeignTitle( 0, '', 'Bogus:Nice_talk' ),
-                       ],
-                       [
-                               'Bogus:Nice_talk', null,
-                               new ForeignTitle( 9000, 'Bogus', 'Nice_talk' ),
-                       ],
-                       [
-                               'Bogus:Nice_talk', 4,
-                               new ForeignTitle( 4, 'Bogus', 'Nice_talk' ),
-                       ],
-                       [
-                               'Bogus:Nice_talk', 1,
-                               new ForeignTitle( 1, 'Talk', 'Nice_talk' ),
-                       ],
-                       // Misconfigured wiki with unregistered namespace (T114115)
-                       [
-                               'Nice_talk', 1234,
-                               new ForeignTitle( 1234, 'Ns1234', 'Nice_talk' ),
-                       ],
-               ];
-       }
-
-       /**
-        * @dataProvider basicProvider
-        */
-       public function testBasic( $title, $ns, ForeignTitle $foreignTitle ) {
-               $foreignNamespaces = [
-                       0 => '', 1 => 'Talk', 100 => 'Portal', 9000 => 'Bogus'
-               ];
-
-               $factory = new NamespaceAwareForeignTitleFactory( $foreignNamespaces );
-               $testTitle = $factory->createForeignTitle( $title, $ns );
-
-               $this->assertEquals( $testTitle->isNamespaceIdKnown(),
-                       $foreignTitle->isNamespaceIdKnown() );
-
-               if (
-                       $testTitle->isNamespaceIdKnown() &&
-                       $foreignTitle->isNamespaceIdKnown()
-               ) {
-                       $this->assertEquals( $testTitle->getNamespaceId(),
-                               $foreignTitle->getNamespaceId() );
-               }
-
-               $this->assertEquals( $testTitle->getNamespaceName(),
-                       $foreignTitle->getNamespaceName() );
-               $this->assertEquals( $testTitle->getText(), $foreignTitle->getText() );
-       }
-}
diff --git a/tests/phpunit/includes/title/TitleValueTest.php b/tests/phpunit/includes/title/TitleValueTest.php
deleted file mode 100644 (file)
index bbeb068..0000000
+++ /dev/null
@@ -1,149 +0,0 @@
-<?php
-/**
- * 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; either version 2 of the License, or
- * (at your option) any later version.
- *
- * 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.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- * @author Daniel Kinzler
- */
-
-/**
- * @covers TitleValue
- *
- * @group Title
- */
-class TitleValueTest extends MediaWikiTestCase {
-
-       public function goodConstructorProvider() {
-               return [
-                       [ NS_MAIN, '', 'fragment', '', true, false ],
-                       [ NS_USER, 'TestThis', 'stuff', '', true, false ],
-                       [ NS_USER, 'TestThis', '', 'baz', false, true ],
-               ];
-       }
-
-       /**
-        * @dataProvider goodConstructorProvider
-        */
-       public function testConstruction( $ns, $text, $fragment, $interwiki, $hasFragment,
-               $hasInterwiki
-       ) {
-               $title = new TitleValue( $ns, $text, $fragment, $interwiki );
-
-               $this->assertEquals( $ns, $title->getNamespace() );
-               $this->assertTrue( $title->inNamespace( $ns ) );
-               $this->assertEquals( $text, $title->getText() );
-               $this->assertEquals( $fragment, $title->getFragment() );
-               $this->assertEquals( $hasFragment, $title->hasFragment() );
-               $this->assertEquals( $interwiki, $title->getInterwiki() );
-               $this->assertEquals( $hasInterwiki, $title->isExternal() );
-       }
-
-       public function badConstructorProvider() {
-               return [
-                       [ 'foo', 'title', 'fragment', '' ],
-                       [ null, 'title', 'fragment', '' ],
-                       [ 2.3, 'title', 'fragment', '' ],
-
-                       [ NS_MAIN, 5, 'fragment', '' ],
-                       [ NS_MAIN, null, 'fragment', '' ],
-                       [ NS_USER, '', 'fragment', '' ],
-                       [ NS_MAIN, 'foo bar', '', '' ],
-                       [ NS_MAIN, 'bar_', '', '' ],
-                       [ NS_MAIN, '_foo', '', '' ],
-                       [ NS_MAIN, ' eek ', '', '' ],
-
-                       [ NS_MAIN, 'title', 5, '' ],
-                       [ NS_MAIN, 'title', null, '' ],
-                       [ NS_MAIN, 'title', [], '' ],
-
-                       [ NS_MAIN, 'title', '', 5 ],
-                       [ NS_MAIN, 'title', null, 5 ],
-                       [ NS_MAIN, 'title', [], 5 ],
-               ];
-       }
-
-       /**
-        * @dataProvider badConstructorProvider
-        */
-       public function testConstructionErrors( $ns, $text, $fragment, $interwiki ) {
-               $this->setExpectedException( InvalidArgumentException::class );
-               new TitleValue( $ns, $text, $fragment, $interwiki );
-       }
-
-       public function fragmentTitleProvider() {
-               return [
-                       [ new TitleValue( NS_MAIN, 'Test' ), 'foo' ],
-                       [ new TitleValue( NS_TALK, 'Test', 'foo' ), '' ],
-                       [ new TitleValue( NS_CATEGORY, 'Test', 'foo' ), 'bar' ],
-               ];
-       }
-
-       /**
-        * @dataProvider fragmentTitleProvider
-        */
-       public function testCreateFragmentTitle( TitleValue $title, $fragment ) {
-               $fragmentTitle = $title->createFragmentTarget( $fragment );
-
-               $this->assertEquals( $title->getNamespace(), $fragmentTitle->getNamespace() );
-               $this->assertEquals( $title->getText(), $fragmentTitle->getText() );
-               $this->assertEquals( $fragment, $fragmentTitle->getFragment() );
-       }
-
-       public function getTextProvider() {
-               return [
-                       [ 'Foo', 'Foo' ],
-                       [ 'Foo_Bar', 'Foo Bar' ],
-               ];
-       }
-
-       /**
-        * @dataProvider getTextProvider
-        */
-       public function testGetText( $dbkey, $text ) {
-               $title = new TitleValue( NS_MAIN, $dbkey );
-
-               $this->assertEquals( $text, $title->getText() );
-       }
-
-       public function provideTestToString() {
-               yield [
-                       new TitleValue( 0, 'Foo' ),
-                       '0:Foo'
-               ];
-               yield [
-                       new TitleValue( 1, 'Bar_Baz' ),
-                       '1:Bar_Baz'
-               ];
-               yield [
-                       new TitleValue( 9, 'JoJo', 'Frag' ),
-                       '9:JoJo#Frag'
-               ];
-               yield [
-                       new TitleValue( 200, 'tea', 'Fragment', 'wikicode' ),
-                       'wikicode:200:tea#Fragment'
-               ];
-       }
-
-       /**
-        * @dataProvider provideTestToString
-        */
-       public function testToString( TitleValue $value, $expected ) {
-               $this->assertSame(
-                       $expected,
-                       $value->__toString()
-               );
-       }
-}
diff --git a/tests/phpunit/includes/user/UserArrayFromResultTest.php b/tests/phpunit/includes/user/UserArrayFromResultTest.php
deleted file mode 100644 (file)
index 4cbfe46..0000000
+++ /dev/null
@@ -1,110 +0,0 @@
-<?php
-
-/**
- * @author Addshore
- * @covers UserArrayFromResult
- */
-class UserArrayFromResultTest extends MediaWikiTestCase {
-
-       private function getMockResultWrapper( $row = null, $numRows = 1 ) {
-               $resultWrapper = $this->getMockBuilder( Wikimedia\Rdbms\ResultWrapper::class )
-                       ->disableOriginalConstructor();
-
-               $resultWrapper = $resultWrapper->getMock();
-               $resultWrapper->expects( $this->atLeastOnce() )
-                       ->method( 'current' )
-                       ->will( $this->returnValue( $row ) );
-               $resultWrapper->expects( $this->any() )
-                       ->method( 'numRows' )
-                       ->will( $this->returnValue( $numRows ) );
-
-               return $resultWrapper;
-       }
-
-       private function getRowWithUsername( $username = 'fooUser' ) {
-               $row = new stdClass();
-               $row->user_name = $username;
-               return $row;
-       }
-
-       /**
-        * @covers UserArrayFromResult::__construct
-        */
-       public function testConstructionWithFalseRow() {
-               $row = false;
-               $resultWrapper = $this->getMockResultWrapper( $row );
-
-               $object = new UserArrayFromResult( $resultWrapper );
-
-               $this->assertEquals( $resultWrapper, $object->res );
-               $this->assertSame( 0, $object->key );
-               $this->assertEquals( $row, $object->current );
-       }
-
-       /**
-        * @covers UserArrayFromResult::__construct
-        */
-       public function testConstructionWithRow() {
-               $username = 'addshore';
-               $row = $this->getRowWithUsername( $username );
-               $resultWrapper = $this->getMockResultWrapper( $row );
-
-               $object = new UserArrayFromResult( $resultWrapper );
-
-               $this->assertEquals( $resultWrapper, $object->res );
-               $this->assertSame( 0, $object->key );
-               $this->assertInstanceOf( User::class, $object->current );
-               $this->assertEquals( $username, $object->current->mName );
-       }
-
-       public static function provideNumberOfRows() {
-               return [
-                       [ 0 ],
-                       [ 1 ],
-                       [ 122 ],
-               ];
-       }
-
-       /**
-        * @dataProvider provideNumberOfRows
-        * @covers UserArrayFromResult::count
-        */
-       public function testCountWithVaryingValues( $numRows ) {
-               $object = new UserArrayFromResult( $this->getMockResultWrapper(
-                       $this->getRowWithUsername(),
-                       $numRows
-               ) );
-               $this->assertEquals( $numRows, $object->count() );
-       }
-
-       /**
-        * @covers UserArrayFromResult::current
-        */
-       public function testCurrentAfterConstruction() {
-               $username = 'addshore';
-               $userRow = $this->getRowWithUsername( $username );
-               $object = new UserArrayFromResult( $this->getMockResultWrapper( $userRow ) );
-               $this->assertInstanceOf( User::class, $object->current() );
-               $this->assertEquals( $username, $object->current()->mName );
-       }
-
-       public function provideTestValid() {
-               return [
-                       [ $this->getRowWithUsername(), true ],
-                       [ false, false ],
-               ];
-       }
-
-       /**
-        * @dataProvider provideTestValid
-        * @covers UserArrayFromResult::valid
-        */
-       public function testValid( $input, $expected ) {
-               $object = new UserArrayFromResult( $this->getMockResultWrapper( $input ) );
-               $this->assertEquals( $expected, $object->valid() );
-       }
-
-       // @todo unit test for key()
-       // @todo unit test for next()
-       // @todo unit test for rewind()
-}
diff --git a/tests/phpunit/includes/utils/AvroValidatorTest.php b/tests/phpunit/includes/utils/AvroValidatorTest.php
deleted file mode 100644 (file)
index cf45f9f..0000000
+++ /dev/null
@@ -1,118 +0,0 @@
-<?php
-/**
- * Tests for IP validity functions.
- *
- * Ported from /t/inc/IP.t by avar.
- *
- * @todo Test methods in this call should be split into a method and a
- * dataprovider.
- */
-
-/**
- * @group IP
- * @covers AvroValidator
- */
-class AvroValidatorTest extends PHPUnit\Framework\TestCase {
-
-       use MediaWikiCoversValidator;
-
-       public function setUp() {
-               if ( !class_exists( 'AvroSchema' ) ) {
-                       $this->markTestSkipped( 'Avro is required to run the AvroValidatorTest' );
-               }
-               parent::setUp();
-       }
-
-       public function getErrorsProvider() {
-               $stringSchema = AvroSchema::parse( json_encode( [ 'type' => 'string' ] ) );
-               $stringArraySchema = AvroSchema::parse( json_encode( [
-                       'type' => 'array',
-                       'items' => 'string',
-               ] ) );
-               $recordSchema = AvroSchema::parse( json_encode( [
-                       'type' => 'record',
-                       'name' => 'ut',
-                       'fields' => [
-                               [ 'name' => 'id', 'type' => 'int', 'required' => true ],
-                       ],
-               ] ) );
-               $enumSchema = AvroSchema::parse( json_encode( [
-                       'type' => 'record',
-                       'name' => 'ut',
-                       'fields' => [
-                               [ 'name' => 'count', 'type' => [ 'int', 'null' ] ],
-                       ],
-               ] ) );
-
-               return [
-                       [
-                               'No errors with a simple string serialization',
-                               $stringSchema, 'foobar', [],
-                       ],
-
-                       [
-                               'Cannot serialize integer into string',
-                               $stringSchema, 5, 'Expected string, but recieved integer',
-                       ],
-
-                       [
-                               'Cannot serialize array into string',
-                               $stringSchema, [], 'Expected string, but recieved array',
-                       ],
-
-                       [
-                               'allows and ignores extra fields',
-                               $recordSchema, [ 'id' => 4, 'foo' => 'bar' ], [],
-                       ],
-
-                       [
-                               'detects missing fields',
-                               $recordSchema, [], [ 'id' => 'Missing expected field' ],
-                       ],
-
-                       [
-                               'handles first element in enum',
-                               $enumSchema, [ 'count' => 4 ], [],
-                       ],
-
-                       [
-                               'handles second element in enum',
-                               $enumSchema, [ 'count' => null ], [],
-                       ],
-
-                       [
-                               'rejects element not in union',
-                               $enumSchema, [ 'count' => 'invalid' ], [ 'count' => [
-                                       'Expected any one of these to be true',
-                                       [
-                                               'Expected integer, but recieved string',
-                                               'Expected null, but recieved string',
-                                       ]
-                               ] ]
-                       ],
-                       [
-                               'Empty array is accepted',
-                               $stringArraySchema, [], []
-                       ],
-                       [
-                               'correct array element accepted',
-                               $stringArraySchema, [ 'fizzbuzz' ], []
-                       ],
-                       [
-                               'incorrect array element rejected',
-                               $stringArraySchema, [ '12', 34 ], [ 'Expected string, but recieved integer' ]
-                       ],
-               ];
-       }
-
-       /**
-        * @dataProvider getErrorsProvider
-        */
-       public function testGetErrors( $message, $schema, $datum, $expected ) {
-               $this->assertEquals(
-                       $expected,
-                       AvroValidator::getErrors( $schema, $datum ),
-                       $message
-               );
-       }
-}
diff --git a/tests/phpunit/includes/utils/BatchRowUpdateTest.php b/tests/phpunit/includes/utils/BatchRowUpdateTest.php
deleted file mode 100644 (file)
index 52b1433..0000000
+++ /dev/null
@@ -1,252 +0,0 @@
-<?php
-
-/**
- * Tests for BatchRowUpdate and its components
- *
- * @group db
- *
- * @covers BatchRowUpdate
- * @covers BatchRowIterator
- * @covers BatchRowWriter
- */
-class BatchRowUpdateTest extends MediaWikiTestCase {
-
-       public function testWriterBasicFunctionality() {
-               $db = $this->mockDb( [ 'update' ] );
-               $writer = new BatchRowWriter( $db, 'echo_event' );
-
-               $updates = [
-                       self::mockUpdate( [ 'something' => 'changed' ] ),
-                       self::mockUpdate( [ 'otherthing' => 'changed' ] ),
-                       self::mockUpdate( [ 'and' => 'something', 'else' => 'changed' ] ),
-               ];
-
-               $db->expects( $this->exactly( count( $updates ) ) )
-                       ->method( 'update' );
-
-               $writer->write( $updates );
-       }
-
-       protected static function mockUpdate( array $changes ) {
-               static $i = 0;
-               return [
-                       'primaryKey' => [ 'event_id' => $i++ ],
-                       'changes' => $changes,
-               ];
-       }
-
-       public function testReaderBasicIterate() {
-               $batchSize = 2;
-               $response = $this->genSelectResult( $batchSize, /*numRows*/ 5, function () {
-                       static $i = 0;
-                       return [ 'id_field' => ++$i ];
-               } );
-               $db = $this->mockDbConsecutiveSelect( $response );
-               $reader = new BatchRowIterator( $db, 'some_table', 'id_field', $batchSize );
-
-               $pos = 0;
-               foreach ( $reader as $rows ) {
-                       $this->assertEquals( $response[$pos], $rows, "Testing row in position $pos" );
-                       $pos++;
-               }
-               // -1 is because the final array() marks the end and isnt included
-               $this->assertEquals( count( $response ) - 1, $pos );
-       }
-
-       public static function provider_readerGetPrimaryKey() {
-               $row = [
-                       'id_field' => 42,
-                       'some_col' => 'dvorak',
-                       'other_col' => 'samurai',
-               ];
-               return [
-
-                       [
-                               'Must return single column pk when requested',
-                               [ 'id_field' => 42 ],
-                               $row
-                       ],
-
-                       [
-                               'Must return multiple column pks when requested',
-                               [ 'id_field' => 42, 'other_col' => 'samurai' ],
-                               $row
-                       ],
-
-               ];
-       }
-
-       /**
-        * @dataProvider provider_readerGetPrimaryKey
-        */
-       public function testReaderGetPrimaryKey( $message, array $expected, array $row ) {
-               $reader = new BatchRowIterator( $this->mockDb(), 'some_table', array_keys( $expected ), 8675309 );
-               $this->assertEquals( $expected, $reader->extractPrimaryKeys( (object)$row ), $message );
-       }
-
-       public static function provider_readerSetFetchColumns() {
-               return [
-
-                       [
-                               'Must merge primary keys into select conditions',
-                               // Expected column select
-                               [ 'foo', 'bar' ],
-                               // primary keys
-                               [ 'foo' ],
-                               // setFetchColumn
-                               [ 'bar' ]
-                       ],
-
-                       [
-                               'Must not merge primary keys into the all columns selector',
-                               // Expected column select
-                               [ '*' ],
-                               // primary keys
-                               [ 'foo' ],
-                               // setFetchColumn
-                               [ '*' ],
-                       ],
-
-                       [
-                               'Must not duplicate primary keys into column selector',
-                               // Expected column select.
-                               // TODO: figure out how to only assert the array_values portion and not the keys
-                               [ 0 => 'foo', 1 => 'bar', 3 => 'baz' ],
-                               // primary keys
-                               [ 'foo', 'bar', ],
-                               // setFetchColumn
-                               [ 'bar', 'baz' ],
-                       ],
-               ];
-       }
-
-       /**
-        * @dataProvider provider_readerSetFetchColumns
-        */
-       public function testReaderSetFetchColumns(
-               $message, array $columns, array $primaryKeys, array $fetchColumns
-       ) {
-               $db = $this->mockDb( [ 'select' ] );
-               $db->expects( $this->once() )
-                       ->method( 'select' )
-                       // only testing second parameter of Database::select
-                       ->with( 'some_table', $columns )
-                       ->will( $this->returnValue( new ArrayIterator( [] ) ) );
-
-               $reader = new BatchRowIterator( $db, 'some_table', $primaryKeys, 22 );
-               $reader->setFetchColumns( $fetchColumns );
-               // triggers first database select
-               $reader->rewind();
-       }
-
-       public static function provider_readerSelectConditions() {
-               return [
-
-                       [
-                               "With single primary key must generate id > 'value'",
-                               // Expected second iteration
-                               [ "( id_field > '3' )" ],
-                               // Primary key(s)
-                               'id_field',
-                       ],
-
-                       [
-                               'With multiple primary keys the first conditions ' .
-                                       'must use >= and the final condition must use >',
-                               // Expected second iteration
-                               [ "( id_field = '3' AND foo > '103' ) OR ( id_field > '3' )" ],
-                               // Primary key(s)
-                               [ 'id_field', 'foo' ],
-                       ],
-
-               ];
-       }
-
-       /**
-        * Slightly hackish to use reflection, but asserting different parameters
-        * to consecutive calls of Database::select in phpunit is error prone
-        *
-        * @dataProvider provider_readerSelectConditions
-        */
-       public function testReaderSelectConditionsMultiplePrimaryKeys(
-               $message, $expectedSecondIteration, $primaryKeys, $batchSize = 3
-       ) {
-               $results = $this->genSelectResult( $batchSize, $batchSize * 3, function () {
-                       static $i = 0, $j = 100, $k = 1000;
-                       return [ 'id_field' => ++$i, 'foo' => ++$j, 'bar' => ++$k ];
-               } );
-               $db = $this->mockDbConsecutiveSelect( $results );
-
-               $conditions = [ 'bar' => 42, 'baz' => 'hai' ];
-               $reader = new BatchRowIterator( $db, 'some_table', $primaryKeys, $batchSize );
-               $reader->addConditions( $conditions );
-
-               $buildConditions = new ReflectionMethod( $reader, 'buildConditions' );
-               $buildConditions->setAccessible( true );
-
-               // On first iteration only the passed conditions must be used
-               $this->assertEquals( $conditions, $buildConditions->invoke( $reader ),
-                       'First iteration must return only the conditions passed in addConditions' );
-               $reader->rewind();
-
-               // Second iteration must use the maximum primary key of last set
-               $this->assertEquals(
-                       $conditions + $expectedSecondIteration,
-                       $buildConditions->invoke( $reader ),
-                       $message
-               );
-       }
-
-       protected function mockDbConsecutiveSelect( array $retvals ) {
-               $db = $this->mockDb( [ 'select', 'addQuotes' ] );
-               $db->expects( $this->any() )
-                       ->method( 'select' )
-                       ->will( $this->consecutivelyReturnFromSelect( $retvals ) );
-               $db->expects( $this->any() )
-                       ->method( 'addQuotes' )
-                       ->will( $this->returnCallback( function ( $value ) {
-                               return "'$value'"; // not real quoting: doesn't matter in test
-                       } ) );
-
-               return $db;
-       }
-
-       protected function consecutivelyReturnFromSelect( array $results ) {
-               $retvals = [];
-               foreach ( $results as $rows ) {
-                       // The Database::select method returns iterators, so we do too.
-                       $retvals[] = $this->returnValue( new ArrayIterator( $rows ) );
-               }
-
-               return call_user_func_array( [ $this, 'onConsecutiveCalls' ], $retvals );
-       }
-
-       protected function genSelectResult( $batchSize, $numRows, $rowGenerator ) {
-               $res = [];
-               for ( $i = 0; $i < $numRows; $i += $batchSize ) {
-                       $rows = [];
-                       for ( $j = 0; $j < $batchSize && $i + $j < $numRows; $j++ ) {
-                               $rows [] = (object)call_user_func( $rowGenerator );
-                       }
-                       $res[] = $rows;
-               }
-               $res[] = []; // termination condition requires empty result for last row
-               return $res;
-       }
-
-       protected function mockDb( $methods = [] ) {
-               // @TODO: mock from Database
-               // FIXME: the constructor normally sets mAtomicLevels and mSrvCache
-               $databaseMysql = $this->getMockBuilder( Wikimedia\Rdbms\DatabaseMysqli::class )
-                       ->disableOriginalConstructor()
-                       ->setMethods( array_merge( [ 'isOpen', 'getApproximateLagStatus' ], $methods ) )
-                       ->getMock();
-               $databaseMysql->expects( $this->any() )
-                       ->method( 'isOpen' )
-                       ->will( $this->returnValue( true ) );
-               $databaseMysql->expects( $this->any() )
-                       ->method( 'getApproximateLagStatus' )
-                       ->will( $this->returnValue( [ 'lag' => 0, 'since' => 0 ] ) );
-               return $databaseMysql;
-       }
-}
diff --git a/tests/phpunit/includes/utils/ClassCollectorTest.php b/tests/phpunit/includes/utils/ClassCollectorTest.php
deleted file mode 100644 (file)
index 9c7c50f..0000000
+++ /dev/null
@@ -1,60 +0,0 @@
-<?php
-
-/**
- * @covers ClassCollector
- */
-class ClassCollectorTest extends PHPUnit\Framework\TestCase {
-
-       use MediaWikiCoversValidator;
-
-       public static function provideCases() {
-               return [
-                       [
-                               "class Foo {}",
-                               [ 'Foo' ],
-                       ],
-                       [
-                               "namespace Example;\nclass Foo {}\nclass Bar {}",
-                               [ 'Example\Foo', 'Example\Bar' ],
-                       ],
-                       [
-                               "class_alias( 'Foo', 'Bar' );",
-                               [ 'Bar' ],
-                       ],
-                       [
-                               "namespace Example;\nclass Foo {}\nclass_alias( 'Example\Foo', 'Foo' );",
-                               [ 'Example\Foo', 'Foo' ],
-                       ],
-                       [
-                               "namespace Example;\nclass Foo {}\nclass_alias( 'Example\Foo', 'Bar' );",
-                               [ 'Example\Foo', 'Bar' ],
-                       ],
-                       [
-                               "class_alias( Foo::class, 'Bar' );",
-                               [ 'Bar' ],
-                       ],
-                       [
-                               // Namespaced class is not currently supported. Must use namespace declaration
-                               // earlier in the file.
-                               "class_alias( Example\Foo::class, 'Bar' );",
-                               [],
-                       ],
-                       [
-                               "namespace Example;\nclass Foo {}\nclass_alias( Foo::class, 'Bar' );",
-                               [ 'Example\Foo', 'Bar' ],
-                       ],
-                       [
-                               "new class() extends Foo {}",
-                               []
-                       ]
-               ];
-       }
-
-       /**
-        * @dataProvider provideCases
-        */
-       public function testGetClasses( $code, array $classes, $message = null ) {
-               $cc = new ClassCollector();
-               $this->assertEquals( $classes, $cc->getClasses( "<?php\n$code" ), $message );
-       }
-}
diff --git a/tests/phpunit/includes/utils/FileContentsHasherTest.php b/tests/phpunit/includes/utils/FileContentsHasherTest.php
deleted file mode 100644 (file)
index 316d9f4..0000000
+++ /dev/null
@@ -1,57 +0,0 @@
-<?php
-
-/**
- * @covers FileContentsHasherTest
- */
-class FileContentsHasherTest extends PHPUnit\Framework\TestCase {
-
-       use MediaWikiCoversValidator;
-
-       public function provideSingleFile() {
-               return array_map( function ( $file ) {
-                       return [ $file, file_get_contents( $file ) ];
-               }, glob( __DIR__ . '/../../data/filecontentshasher/*.*' ) );
-       }
-
-       public function provideMultipleFiles() {
-               return [
-                       [ $this->provideSingleFile() ]
-               ];
-       }
-
-       /**
-        * @covers FileContentsHasher::getFileContentsHash
-        * @covers FileContentsHasher::getFileContentsHashInternal
-        * @dataProvider provideSingleFile
-        */
-       public function testSingleFileHash( $fileName, $contents ) {
-               foreach ( [ 'md4', 'md5' ] as $algo ) {
-                       $expectedHash = hash( $algo, $contents );
-                       $actualHash = FileContentsHasher::getFileContentsHash( $fileName, $algo );
-                       $this->assertEquals( $expectedHash, $actualHash );
-                       $actualHashRepeat = FileContentsHasher::getFileContentsHash( $fileName, $algo );
-                       $this->assertEquals( $expectedHash, $actualHashRepeat );
-               }
-       }
-
-       /**
-        * @covers FileContentsHasher::getFileContentsHash
-        * @covers FileContentsHasher::getFileContentsHashInternal
-        * @dataProvider provideMultipleFiles
-        */
-       public function testMultipleFileHash( $files ) {
-               $fileNames = [];
-               $hashes = [];
-               foreach ( $files as $fileInfo ) {
-                       list( $fileName, $contents ) = $fileInfo;
-                       $fileNames[] = $fileName;
-                       $hashes[] = md5( $contents );
-               }
-
-               $expectedHash = md5( implode( '', $hashes ) );
-               $actualHash = FileContentsHasher::getFileContentsHash( $fileNames, 'md5' );
-               $this->assertEquals( $expectedHash, $actualHash );
-               $actualHashRepeat = FileContentsHasher::getFileContentsHash( $fileNames, 'md5' );
-               $this->assertEquals( $expectedHash, $actualHashRepeat );
-       }
-}
diff --git a/tests/phpunit/includes/utils/MWCryptHashTest.php b/tests/phpunit/includes/utils/MWCryptHashTest.php
deleted file mode 100644 (file)
index 94705bf..0000000
+++ /dev/null
@@ -1,64 +0,0 @@
-<?php
-
-/**
- * @group Hash
- *
- * @covers MWCryptHash
- */
-class MWCryptHashTest extends PHPUnit\Framework\TestCase {
-
-       use MediaWikiCoversValidator;
-
-       public function testHashLength() {
-               if ( MWCryptHash::hashAlgo() !== 'whirlpool' ) {
-                       $this->markTestSkipped( 'Hash algorithm isn\'t whirlpool' );
-               }
-
-               $this->assertEquals( 64, MWCryptHash::hashLength(), 'Raw hash length' );
-               $this->assertEquals( 128, MWCryptHash::hashLength( false ), 'Hex hash length' );
-       }
-
-       public function testHash() {
-               if ( MWCryptHash::hashAlgo() !== 'whirlpool' ) {
-                       $this->markTestSkipped( 'Hash algorithm isn\'t whirlpool' );
-               }
-
-               $data = 'foobar';
-               // phpcs:ignore Generic.Files.LineLength
-               $hash = '9923afaec3a86f865bb231a588f453f84e8151a2deb4109aebc6de4284be5bebcff4fab82a7e51d920237340a043736e9d13bab196006dcca0fe65314d68eab9';
-
-               $this->assertEquals(
-                       hex2bin( $hash ),
-                       MWCryptHash::hash( $data ),
-                       'Raw hash'
-               );
-               $this->assertEquals(
-                       $hash,
-                       MWCryptHash::hash( $data, false ),
-                       'Hex hash'
-               );
-       }
-
-       public function testHmac() {
-               if ( MWCryptHash::hashAlgo() !== 'whirlpool' ) {
-                       $this->markTestSkipped( 'Hash algorithm isn\'t whirlpool' );
-               }
-
-               $data = 'foobar';
-               $key = 'secret';
-               // phpcs:ignore Generic.Files.LineLength
-               $hash = 'ddc94177b2020e55ce2049199fd9cc6327f416ff6dc621cc34cb43d9bec61d73372b4790c0e24957f565ecaf2d42821e6303619093e99cbe14a3b9250bda5f81';
-
-               $this->assertEquals(
-                       hex2bin( $hash ),
-                       MWCryptHash::hmac( $data, $key ),
-                       'Raw hmac'
-               );
-               $this->assertEquals(
-                       $hash,
-                       MWCryptHash::hmac( $data, $key, false ),
-                       'Hex hmac'
-               );
-       }
-
-}
diff --git a/tests/phpunit/includes/utils/MWRestrictionsTest.php b/tests/phpunit/includes/utils/MWRestrictionsTest.php
deleted file mode 100644 (file)
index abdfbb1..0000000
+++ /dev/null
@@ -1,217 +0,0 @@
-<?php
-class MWRestrictionsTest extends PHPUnit\Framework\TestCase {
-
-       use MediaWikiCoversValidator;
-
-       protected static $restrictionsForChecks;
-
-       public static function setUpBeforeClass() {
-               self::$restrictionsForChecks = MWRestrictions::newFromArray( [
-                       'IPAddresses' => [
-                               '10.0.0.0/8',
-                               '172.16.0.0/12',
-                               '2001:db8::/33',
-                       ]
-               ] );
-       }
-
-       /**
-        * @covers MWRestrictions::newDefault
-        * @covers MWRestrictions::__construct
-        */
-       public function testNewDefault() {
-               $ret = MWRestrictions::newDefault();
-               $this->assertInstanceOf( MWRestrictions::class, $ret );
-               $this->assertSame(
-                       '{"IPAddresses":["0.0.0.0/0","::/0"]}',
-                       $ret->toJson()
-               );
-       }
-
-       /**
-        * @covers MWRestrictions::newFromArray
-        * @covers MWRestrictions::__construct
-        * @covers MWRestrictions::loadFromArray
-        * @covers MWRestrictions::toArray
-        * @dataProvider provideArray
-        * @param array $data
-        * @param bool|InvalidArgumentException $expect True if the call succeeds,
-        *  otherwise the exception that should be thrown.
-        */
-       public function testArray( $data, $expect ) {
-               if ( $expect === true ) {
-                       $ret = MWRestrictions::newFromArray( $data );
-                       $this->assertInstanceOf( MWRestrictions::class, $ret );
-                       $this->assertSame( $data, $ret->toArray() );
-               } else {
-                       try {
-                               MWRestrictions::newFromArray( $data );
-                               $this->fail( 'Expected exception not thrown' );
-                       } catch ( InvalidArgumentException $ex ) {
-                               $this->assertEquals( $expect, $ex );
-                       }
-               }
-       }
-
-       public static function provideArray() {
-               return [
-                       [ [ 'IPAddresses' => [] ], true ],
-                       [ [ 'IPAddresses' => [ '127.0.0.1/32' ] ], true ],
-                       [
-                               [ 'IPAddresses' => [ '256.0.0.1/32' ] ],
-                               new InvalidArgumentException( 'Invalid IP address: 256.0.0.1/32' )
-                       ],
-                       [
-                               [ 'IPAddresses' => '127.0.0.1/32' ],
-                               new InvalidArgumentException( 'IPAddresses is not an array' )
-                       ],
-                       [
-                               [],
-                               new InvalidArgumentException( 'Array is missing required keys: IPAddresses' )
-                       ],
-                       [
-                               [ 'foo' => 'bar', 'bar' => 42 ],
-                               new InvalidArgumentException( 'Array contains invalid keys: foo, bar' )
-                       ],
-               ];
-       }
-
-       /**
-        * @covers MWRestrictions::newFromJson
-        * @covers MWRestrictions::__construct
-        * @covers MWRestrictions::loadFromArray
-        * @covers MWRestrictions::toJson
-        * @covers MWRestrictions::__toString
-        * @dataProvider provideJson
-        * @param string $json
-        * @param array|InvalidArgumentException $expect
-        */
-       public function testJson( $json, $expect ) {
-               if ( is_array( $expect ) ) {
-                       $ret = MWRestrictions::newFromJson( $json );
-                       $this->assertInstanceOf( MWRestrictions::class, $ret );
-                       $this->assertSame( $expect, $ret->toArray() );
-
-                       $this->assertSame( $json, $ret->toJson( false ) );
-                       $this->assertSame( $json, (string)$ret );
-
-                       $this->assertSame(
-                               FormatJson::encode( $expect, true, FormatJson::ALL_OK ),
-                               $ret->toJson( true )
-                       );
-               } else {
-                       try {
-                               MWRestrictions::newFromJson( $json );
-                               $this->fail( 'Expected exception not thrown' );
-                       } catch ( InvalidArgumentException $ex ) {
-                               $this->assertTrue( true );
-                       }
-               }
-       }
-
-       public static function provideJson() {
-               return [
-                       [
-                               '{"IPAddresses":[]}',
-                               [ 'IPAddresses' => [] ]
-                       ],
-                       [
-                               '{"IPAddresses":["127.0.0.1/32"]}',
-                               [ 'IPAddresses' => [ '127.0.0.1/32' ] ]
-                       ],
-                       [
-                               '{"IPAddresses":["256.0.0.1/32"]}',
-                               new InvalidArgumentException( 'Invalid IP address: 256.0.0.1/32' )
-                       ],
-                       [
-                               '{"IPAddresses":"127.0.0.1/32"}',
-                               new InvalidArgumentException( 'IPAddresses is not an array' )
-                       ],
-                       [
-                               '{}',
-                               new InvalidArgumentException( 'Array is missing required keys: IPAddresses' )
-                       ],
-                       [
-                               '{"foo":"bar","bar":42}',
-                               new InvalidArgumentException( 'Array contains invalid keys: foo, bar' )
-                       ],
-                       [
-                               '{"IPAddresses":[]',
-                               new InvalidArgumentException( 'Invalid restrictions JSON' )
-                       ],
-                       [
-                               '"IPAddresses"',
-                               new InvalidArgumentException( 'Invalid restrictions JSON' )
-                       ],
-               ];
-       }
-
-       /**
-        * @covers MWRestrictions::checkIP
-        * @dataProvider provideCheckIP
-        * @param string $ip
-        * @param bool $pass
-        */
-       public function testCheckIP( $ip, $pass ) {
-               $this->assertSame( $pass, self::$restrictionsForChecks->checkIP( $ip ) );
-       }
-
-       public static function provideCheckIP() {
-               return [
-                       [ '10.0.0.1', true ],
-                       [ '172.16.0.0', true ],
-                       [ '192.0.2.1', false ],
-                       [ '2001:db8:1::', true ],
-                       [ '2001:0db8:0000:0000:0000:0000:0000:0000', true ],
-                       [ '2001:0DB8:8000::', false ],
-               ];
-       }
-
-       /**
-        * @covers MWRestrictions::check
-        * @dataProvider provideCheck
-        * @param WebRequest $request
-        * @param Status $expect
-        */
-       public function testCheck( $request, $expect ) {
-               $this->assertEquals( $expect, self::$restrictionsForChecks->check( $request ) );
-       }
-
-       public function provideCheck() {
-               $ret = [];
-
-               $mockBuilder = $this->getMockBuilder( FauxRequest::class )
-                       ->setMethods( [ 'getIP' ] );
-
-               foreach ( self::provideCheckIP() as $checkIP ) {
-                       $ok = [];
-                       $request = $mockBuilder->getMock();
-
-                       $request->expects( $this->any() )->method( 'getIP' )
-                               ->will( $this->returnValue( $checkIP[0] ) );
-                       $ok['ip'] = $checkIP[1];
-
-                       /* If we ever add more restrictions, add nested for loops here:
-                        *  foreach ( self::provideCheckFoo() as $checkFoo ) {
-                        *      $request->expects( $this->any() )->method( 'getFoo' )
-                        *          ->will( $this->returnValue( $checkFoo[0] );
-                        *      $ok['foo'] = $checkFoo[1];
-                        *
-                        *      foreach ( self::provideCheckBar() as $checkBar ) {
-                        *          $request->expects( $this->any() )->method( 'getBar' )
-                        *              ->will( $this->returnValue( $checkBar[0] );
-                        *          $ok['bar'] = $checkBar[1];
-                        *
-                        *          // etc.
-                        *      }
-                        *  }
-                        */
-
-                       $status = Status::newGood();
-                       $status->setResult( $ok === array_filter( $ok ), $ok );
-                       $ret[] = [ $request, $status ];
-               }
-
-               return $ret;
-       }
-}
diff --git a/tests/phpunit/includes/utils/UIDGeneratorTest.php b/tests/phpunit/includes/utils/UIDGeneratorTest.php
deleted file mode 100644 (file)
index 6b81a66..0000000
+++ /dev/null
@@ -1,177 +0,0 @@
-<?php
-
-class UIDGeneratorTest extends PHPUnit\Framework\TestCase {
-
-       use MediaWikiCoversValidator;
-
-       protected function tearDown() {
-               // T46850
-               UIDGenerator::unitTestTearDown();
-               parent::tearDown();
-       }
-
-       /**
-        * Test that generated UIDs have the expected properties
-        *
-        * @dataProvider provider_testTimestampedUID
-        * @covers UIDGenerator::newTimestampedUID88
-        * @covers UIDGenerator::getTimestampedID88
-        * @covers UIDGenerator::newTimestampedUID128
-        * @covers UIDGenerator::getTimestampedID128
-        */
-       public function testTimestampedUID( $method, $digitlen, $bits, $tbits, $hostbits ) {
-               $id = call_user_func( [ UIDGenerator::class, $method ] );
-               $this->assertEquals( true, ctype_digit( $id ), "UID made of digit characters" );
-               $this->assertLessThanOrEqual( $digitlen, strlen( $id ),
-                       "UID has the right number of digits" );
-               $this->assertLessThanOrEqual( $bits, strlen( Wikimedia\base_convert( $id, 10, 2 ) ),
-                       "UID has the right number of bits" );
-
-               $ids = [];
-               for ( $i = 0; $i < 300; $i++ ) {
-                       $ids[] = call_user_func( [ UIDGenerator::class, $method ] );
-               }
-
-               $lastId = array_shift( $ids );
-
-               $this->assertSame( array_unique( $ids ), $ids, "All generated IDs are unique." );
-
-               foreach ( $ids as $id ) {
-                       // Convert string to binary and pad to full length so we can
-                       // extract segments
-                       $id_bin = Wikimedia\base_convert( $id, 10, 2, $bits );
-                       $lastId_bin = Wikimedia\base_convert( $lastId, 10, 2, $bits );
-
-                       $timestamp_bin = substr( $id_bin, 0, $tbits );
-                       $last_timestamp_bin = substr( $lastId_bin, 0, $tbits );
-
-                       $this->assertGreaterThanOrEqual(
-                               $last_timestamp_bin,
-                               $timestamp_bin,
-                               "timestamp ($timestamp_bin) of current ID ($id_bin) >= timestamp ($last_timestamp_bin) " .
-                                       "of prior one ($lastId_bin)" );
-
-                       $hostbits_bin = substr( $id_bin, -$hostbits );
-                       $last_hostbits_bin = substr( $lastId_bin, -$hostbits );
-
-                       if ( $hostbits ) {
-                               $this->assertEquals(
-                                       $hostbits_bin,
-                                       $last_hostbits_bin,
-                                       "Host ID ($hostbits_bin) of current ID ($id_bin) is same as host ID ($last_hostbits_bin) " .
-                                               "of prior one ($lastId_bin)." );
-                       }
-
-                       $lastId = $id;
-               }
-       }
-
-       /**
-        * array( method, length, bits, hostbits )
-        * NOTE: When adding a new method name here please update the covers tags for the tests!
-        */
-       public static function provider_testTimestampedUID() {
-               return [
-                       [ 'newTimestampedUID128', 39, 128, 46, 48 ],
-                       [ 'newTimestampedUID128', 39, 128, 46, 48 ],
-                       [ 'newTimestampedUID88', 27, 88, 46, 32 ],
-               ];
-       }
-
-       /**
-        * @covers UIDGenerator::newUUIDv1
-        * @covers UIDGenerator::getUUIDv1
-        */
-       public function testUUIDv1() {
-               $ids = [];
-               for ( $i = 0; $i < 100; $i++ ) {
-                       $id = UIDGenerator::newUUIDv1();
-                       $this->assertEquals( true,
-                               preg_match( '!^[0-9a-f]{8}-[0-9a-f]{4}-1[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$!', $id ),
-                               "UID $id has the right format" );
-                       $ids[] = $id;
-
-                       $id = UIDGenerator::newRawUUIDv1();
-                       $this->assertEquals( true,
-                               preg_match( '!^[0-9a-f]{12}1[0-9a-f]{3}[89ab][0-9a-f]{15}$!', $id ),
-                               "UID $id has the right format" );
-
-                       $id = UIDGenerator::newRawUUIDv1();
-                       $this->assertEquals( true,
-                               preg_match( '!^[0-9a-f]{12}1[0-9a-f]{3}[89ab][0-9a-f]{15}$!', $id ),
-                               "UID $id has the right format" );
-               }
-
-               $this->assertEquals( array_unique( $ids ), $ids, "All generated IDs are unique." );
-       }
-
-       /**
-        * @covers UIDGenerator::newUUIDv4
-        */
-       public function testUUIDv4() {
-               $ids = [];
-               for ( $i = 0; $i < 100; $i++ ) {
-                       $id = UIDGenerator::newUUIDv4();
-                       $ids[] = $id;
-                       $this->assertEquals( true,
-                               preg_match( '!^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$!', $id ),
-                               "UID $id has the right format" );
-               }
-
-               $this->assertEquals( array_unique( $ids ), $ids, 'All generated IDs are unique.' );
-       }
-
-       /**
-        * @covers UIDGenerator::newRawUUIDv4
-        */
-       public function testRawUUIDv4() {
-               for ( $i = 0; $i < 100; $i++ ) {
-                       $id = UIDGenerator::newRawUUIDv4();
-                       $this->assertEquals( true,
-                               preg_match( '!^[0-9a-f]{12}4[0-9a-f]{3}[89ab][0-9a-f]{15}$!', $id ),
-                               "UID $id has the right format" );
-               }
-       }
-
-       /**
-        * @covers UIDGenerator::newRawUUIDv4
-        */
-       public function testRawUUIDv4QuickRand() {
-               for ( $i = 0; $i < 100; $i++ ) {
-                       $id = UIDGenerator::newRawUUIDv4( UIDGenerator::QUICK_RAND );
-                       $this->assertEquals( true,
-                               preg_match( '!^[0-9a-f]{12}4[0-9a-f]{3}[89ab][0-9a-f]{15}$!', $id ),
-                               "UID $id has the right format" );
-               }
-       }
-
-       /**
-        * @covers UIDGenerator::newSequentialPerNodeID
-        */
-       public function testNewSequentialID() {
-               $id1 = UIDGenerator::newSequentialPerNodeID( 'test', 32 );
-               $id2 = UIDGenerator::newSequentialPerNodeID( 'test', 32 );
-
-               $this->assertInternalType( 'float', $id1, "ID returned as float" );
-               $this->assertInternalType( 'float', $id2, "ID returned as float" );
-               $this->assertGreaterThan( 0, $id1, "ID greater than 1" );
-               $this->assertGreaterThan( $id1, $id2, "IDs increasing in value" );
-       }
-
-       /**
-        * @covers UIDGenerator::newSequentialPerNodeIDs
-        * @covers UIDGenerator::getSequentialPerNodeIDs
-        */
-       public function testNewSequentialIDs() {
-               $ids = UIDGenerator::newSequentialPerNodeIDs( 'test', 32, 5 );
-               $lastId = null;
-               foreach ( $ids as $id ) {
-                       $this->assertInternalType( 'float', $id, "ID returned as float" );
-                       $this->assertGreaterThan( 0, $id, "ID greater than 1" );
-                       if ( $lastId ) {
-                               $this->assertGreaterThan( $lastId, $id, "IDs increasing in value" );
-                       }
-                       $lastId = $id;
-               }
-       }
-}
diff --git a/tests/phpunit/includes/utils/ZipDirectoryReaderTest.php b/tests/phpunit/includes/utils/ZipDirectoryReaderTest.php
deleted file mode 100644 (file)
index a1a3fd7..0000000
+++ /dev/null
@@ -1,88 +0,0 @@
-<?php
-
-/**
- * @covers ZipDirectoryReader
- * NOTE: this test is more like an integration test than a unit test
- */
-class ZipDirectoryReaderTest extends PHPUnit\Framework\TestCase {
-
-       use MediaWikiCoversValidator;
-
-       protected $zipDir;
-       protected $entries;
-
-       protected function setUp() {
-               parent::setUp();
-               $this->zipDir = __DIR__ . '/../../data/zip';
-       }
-
-       function zipCallback( $entry ) {
-               $this->entries[] = $entry;
-       }
-
-       function readZipAssertError( $file, $error, $assertMessage ) {
-               $this->entries = [];
-               $status = ZipDirectoryReader::read( "{$this->zipDir}/$file", [ $this, 'zipCallback' ] );
-               $this->assertTrue( $status->hasMessage( $error ), $assertMessage );
-       }
-
-       function readZipAssertSuccess( $file, $assertMessage ) {
-               $this->entries = [];
-               $status = ZipDirectoryReader::read( "{$this->zipDir}/$file", [ $this, 'zipCallback' ] );
-               $this->assertTrue( $status->isOK(), $assertMessage );
-       }
-
-       public function testEmpty() {
-               $this->readZipAssertSuccess( 'empty.zip', 'Empty zip' );
-       }
-
-       public function testMultiDisk0() {
-               $this->readZipAssertError( 'split.zip', 'zip-unsupported',
-                       'Split zip error' );
-       }
-
-       public function testNoSignature() {
-               $this->readZipAssertError( 'nosig.zip', 'zip-wrong-format',
-                       'No signature should give "wrong format" error' );
-       }
-
-       public function testSimple() {
-               $this->readZipAssertSuccess( 'class.zip', 'Simple ZIP' );
-               $this->assertEquals( $this->entries, [ [
-                       'name' => 'Class.class',
-                       'mtime' => '20010115000000',
-                       'size' => 1,
-               ] ] );
-       }
-
-       public function testBadCentralEntrySignature() {
-               $this->readZipAssertError( 'wrong-central-entry-sig.zip', 'zip-bad',
-                       'Bad central entry error' );
-       }
-
-       public function testTrailingBytes() {
-               // Due to T40432 this is now zip-wrong-format instead of zip-bad
-               $this->readZipAssertError( 'trail.zip', 'zip-wrong-format',
-                       'Trailing bytes error' );
-       }
-
-       public function testWrongCDStart() {
-               $this->readZipAssertError( 'wrong-cd-start-disk.zip', 'zip-unsupported',
-                       'Wrong CD start disk error' );
-       }
-
-       public function testCentralDirectoryGap() {
-               $this->readZipAssertError( 'cd-gap.zip', 'zip-bad',
-                       'CD gap error' );
-       }
-
-       public function testCentralDirectoryTruncated() {
-               $this->readZipAssertError( 'cd-truncated.zip', 'zip-bad',
-                       'CD truncated error (should hit unpack() overrun)' );
-       }
-
-       public function testLooksLikeZip64() {
-               $this->readZipAssertError( 'looks-like-zip64.zip', 'zip-unsupported',
-                       'A file which looks like ZIP64 but isn\'t, should give error' );
-       }
-}
diff --git a/tests/phpunit/includes/watcheditem/NoWriteWatchedItemStoreUnitTest.php b/tests/phpunit/includes/watcheditem/NoWriteWatchedItemStoreUnitTest.php
deleted file mode 100644 (file)
index f424b21..0000000
+++ /dev/null
@@ -1,250 +0,0 @@
-<?php
-
-use MediaWiki\User\UserIdentityValue;
-
-/**
- * @author Addshore
- *
- * @covers NoWriteWatchedItemStore
- */
-class NoWriteWatchedItemStoreUnitTest extends MediaWikiTestCase {
-
-       public function testAddWatch() {
-               /** @var WatchedItemStoreInterface|PHPUnit_Framework_MockObject_MockObject $innerService */
-               $innerService = $this->getMockForAbstractClass( WatchedItemStoreInterface::class );
-               $innerService->expects( $this->never() )->method( 'addWatch' );
-               $noWriteService = new NoWriteWatchedItemStore( $innerService );
-
-               $this->setExpectedException( DBReadOnlyError::class );
-               $noWriteService->addWatch(
-                       new UserIdentityValue( 1, 'MockUser', 0 ), new TitleValue( 0, 'Foo' ) );
-       }
-
-       public function testAddWatchBatchForUser() {
-               /** @var WatchedItemStoreInterface|PHPUnit_Framework_MockObject_MockObject $innerService */
-               $innerService = $this->getMockForAbstractClass( WatchedItemStoreInterface::class );
-               $innerService->expects( $this->never() )->method( 'addWatchBatchForUser' );
-               $noWriteService = new NoWriteWatchedItemStore( $innerService );
-
-               $this->setExpectedException( DBReadOnlyError::class );
-               $noWriteService->addWatchBatchForUser( new UserIdentityValue( 1, 'MockUser', 0 ), [] );
-       }
-
-       public function testRemoveWatch() {
-               /** @var WatchedItemStoreInterface|PHPUnit_Framework_MockObject_MockObject $innerService */
-               $innerService = $this->getMockForAbstractClass( WatchedItemStoreInterface::class );
-               $innerService->expects( $this->never() )->method( 'removeWatch' );
-               $noWriteService = new NoWriteWatchedItemStore( $innerService );
-
-               $this->setExpectedException( DBReadOnlyError::class );
-               $noWriteService->removeWatch(
-                       new UserIdentityValue( 1, 'MockUser', 0 ), new TitleValue( 0, 'Foo' ) );
-       }
-
-       public function testSetNotificationTimestampsForUser() {
-               /** @var WatchedItemStoreInterface|PHPUnit_Framework_MockObject_MockObject $innerService */
-               $innerService = $this->getMockForAbstractClass( WatchedItemStoreInterface::class );
-               $innerService->expects( $this->never() )->method( 'setNotificationTimestampsForUser' );
-               $noWriteService = new NoWriteWatchedItemStore( $innerService );
-
-               $this->setExpectedException( DBReadOnlyError::class );
-               $noWriteService->setNotificationTimestampsForUser(
-                       new UserIdentityValue( 1, 'MockUser', 0 ),
-                       'timestamp',
-                       []
-               );
-       }
-
-       public function testUpdateNotificationTimestamp() {
-               /** @var WatchedItemStoreInterface|PHPUnit_Framework_MockObject_MockObject $innerService */
-               $innerService = $this->getMockForAbstractClass( WatchedItemStoreInterface::class );
-               $innerService->expects( $this->never() )->method( 'updateNotificationTimestamp' );
-               $noWriteService = new NoWriteWatchedItemStore( $innerService );
-
-               $this->setExpectedException( DBReadOnlyError::class );
-               $noWriteService->updateNotificationTimestamp(
-                       new UserIdentityValue( 1, 'MockUser', 0 ),
-                       new TitleValue( 0, 'Foo' ),
-                       'timestamp'
-               );
-       }
-
-       public function testResetNotificationTimestamp() {
-               /** @var WatchedItemStoreInterface|PHPUnit_Framework_MockObject_MockObject $innerService */
-               $innerService = $this->getMockForAbstractClass( WatchedItemStoreInterface::class );
-               $innerService->expects( $this->never() )->method( 'resetNotificationTimestamp' );
-               $noWriteService = new NoWriteWatchedItemStore( $innerService );
-
-               $this->setExpectedException( DBReadOnlyError::class );
-               $noWriteService->resetNotificationTimestamp(
-                       new UserIdentityValue( 1, 'MockUser', 0 ),
-                       new TitleValue( 0, 'Foo' )
-               );
-       }
-
-       public function testCountWatchedItems() {
-               /** @var WatchedItemStoreInterface|PHPUnit_Framework_MockObject_MockObject $innerService */
-               $innerService = $this->getMockForAbstractClass( WatchedItemStoreInterface::class );
-               $innerService->expects( $this->once() )->method( 'countWatchedItems' )->willReturn( __METHOD__ );
-               $noWriteService = new NoWriteWatchedItemStore( $innerService );
-
-               $return = $noWriteService->countWatchedItems(
-                       new UserIdentityValue( 1, 'MockUser', 0 )
-               );
-               $this->assertEquals( __METHOD__, $return );
-       }
-
-       public function testCountWatchers() {
-               /** @var WatchedItemStoreInterface|PHPUnit_Framework_MockObject_MockObject $innerService */
-               $innerService = $this->getMockForAbstractClass( WatchedItemStoreInterface::class );
-               $innerService->expects( $this->once() )->method( 'countWatchers' )->willReturn( __METHOD__ );
-               $noWriteService = new NoWriteWatchedItemStore( $innerService );
-
-               $return = $noWriteService->countWatchers(
-                       new TitleValue( 0, 'Foo' )
-               );
-               $this->assertEquals( __METHOD__, $return );
-       }
-
-       public function testCountVisitingWatchers() {
-               /** @var WatchedItemStoreInterface|PHPUnit_Framework_MockObject_MockObject $innerService */
-               $innerService = $this->getMockForAbstractClass( WatchedItemStoreInterface::class );
-               $innerService->expects( $this->once() )
-                       ->method( 'countVisitingWatchers' )
-                       ->willReturn( __METHOD__ );
-               $noWriteService = new NoWriteWatchedItemStore( $innerService );
-
-               $return = $noWriteService->countVisitingWatchers(
-                       new TitleValue( 0, 'Foo' ),
-                       9
-               );
-               $this->assertEquals( __METHOD__, $return );
-       }
-
-       public function testCountWatchersMultiple() {
-               /** @var WatchedItemStoreInterface|PHPUnit_Framework_MockObject_MockObject $innerService */
-               $innerService = $this->getMockForAbstractClass( WatchedItemStoreInterface::class );
-               $innerService->expects( $this->once() )
-                       ->method( 'countVisitingWatchersMultiple' )
-                       ->willReturn( __METHOD__ );
-               $noWriteService = new NoWriteWatchedItemStore( $innerService );
-
-               $return = $noWriteService->countWatchersMultiple(
-                       [ new TitleValue( 0, 'Foo' ) ],
-                       []
-               );
-               $this->assertEquals( __METHOD__, $return );
-       }
-
-       public function testCountVisitingWatchersMultiple() {
-               /** @var WatchedItemStoreInterface|PHPUnit_Framework_MockObject_MockObject $innerService */
-               $innerService = $this->getMockForAbstractClass( WatchedItemStoreInterface::class );
-               $innerService->expects( $this->once() )
-                       ->method( 'countVisitingWatchersMultiple' )
-                       ->willReturn( __METHOD__ );
-               $noWriteService = new NoWriteWatchedItemStore( $innerService );
-
-               $return = $noWriteService->countVisitingWatchersMultiple(
-                       [ [ new TitleValue( 0, 'Foo' ), 99 ] ],
-                       11
-               );
-               $this->assertEquals( __METHOD__, $return );
-       }
-
-       public function testGetWatchedItem() {
-               /** @var WatchedItemStoreInterface|PHPUnit_Framework_MockObject_MockObject $innerService */
-               $innerService = $this->getMockForAbstractClass( WatchedItemStoreInterface::class );
-               $innerService->expects( $this->once() )->method( 'getWatchedItem' )->willReturn( __METHOD__ );
-               $noWriteService = new NoWriteWatchedItemStore( $innerService );
-
-               $return = $noWriteService->getWatchedItem(
-                       new UserIdentityValue( 1, 'MockUser', 0 ),
-                       new TitleValue( 0, 'Foo' )
-               );
-               $this->assertEquals( __METHOD__, $return );
-       }
-
-       public function testLoadWatchedItem() {
-               /** @var WatchedItemStoreInterface|PHPUnit_Framework_MockObject_MockObject $innerService */
-               $innerService = $this->getMockForAbstractClass( WatchedItemStoreInterface::class );
-               $innerService->expects( $this->once() )->method( 'loadWatchedItem' )->willReturn( __METHOD__ );
-               $noWriteService = new NoWriteWatchedItemStore( $innerService );
-
-               $return = $noWriteService->loadWatchedItem(
-                       new UserIdentityValue( 1, 'MockUser', 0 ),
-                       new TitleValue( 0, 'Foo' )
-               );
-               $this->assertEquals( __METHOD__, $return );
-       }
-
-       public function testGetWatchedItemsForUser() {
-               /** @var WatchedItemStoreInterface|PHPUnit_Framework_MockObject_MockObject $innerService */
-               $innerService = $this->getMockForAbstractClass( WatchedItemStoreInterface::class );
-               $innerService->expects( $this->once() )
-                       ->method( 'getWatchedItemsForUser' )
-                       ->willReturn( __METHOD__ );
-               $noWriteService = new NoWriteWatchedItemStore( $innerService );
-
-               $return = $noWriteService->getWatchedItemsForUser(
-                       new UserIdentityValue( 1, 'MockUser', 0 ),
-                       []
-               );
-               $this->assertEquals( __METHOD__, $return );
-       }
-
-       public function testIsWatched() {
-               /** @var WatchedItemStoreInterface|PHPUnit_Framework_MockObject_MockObject $innerService */
-               $innerService = $this->getMockForAbstractClass( WatchedItemStoreInterface::class );
-               $innerService->expects( $this->once() )->method( 'isWatched' )->willReturn( __METHOD__ );
-               $noWriteService = new NoWriteWatchedItemStore( $innerService );
-
-               $return = $noWriteService->isWatched(
-                       new UserIdentityValue( 1, 'MockUser', 0 ),
-                       new TitleValue( 0, 'Foo' )
-               );
-               $this->assertEquals( __METHOD__, $return );
-       }
-
-       public function testGetNotificationTimestampsBatch() {
-               /** @var WatchedItemStoreInterface|PHPUnit_Framework_MockObject_MockObject $innerService */
-               $innerService = $this->getMockForAbstractClass( WatchedItemStoreInterface::class );
-               $innerService->expects( $this->once() )
-                       ->method( 'getNotificationTimestampsBatch' )
-                       ->willReturn( __METHOD__ );
-               $noWriteService = new NoWriteWatchedItemStore( $innerService );
-
-               $return = $noWriteService->getNotificationTimestampsBatch(
-                       new UserIdentityValue( 1, 'MockUser', 0 ),
-                       [ new TitleValue( 0, 'Foo' ) ]
-               );
-               $this->assertEquals( __METHOD__, $return );
-       }
-
-       public function testCountUnreadNotifications() {
-               /** @var WatchedItemStoreInterface|PHPUnit_Framework_MockObject_MockObject $innerService */
-               $innerService = $this->getMockForAbstractClass( WatchedItemStoreInterface::class );
-               $innerService->expects( $this->once() )
-                       ->method( 'countUnreadNotifications' )
-                       ->willReturn( __METHOD__ );
-               $noWriteService = new NoWriteWatchedItemStore( $innerService );
-
-               $return = $noWriteService->countUnreadNotifications(
-                       new UserIdentityValue( 1, 'MockUser', 0 ),
-                       88
-               );
-               $this->assertEquals( __METHOD__, $return );
-       }
-
-       public function testDuplicateAllAssociatedEntries() {
-               /** @var WatchedItemStoreInterface|PHPUnit_Framework_MockObject_MockObject $innerService */
-               $innerService = $this->getMockForAbstractClass( WatchedItemStoreInterface::class );
-               $noWriteService = new NoWriteWatchedItemStore( $innerService );
-
-               $this->setExpectedException( DBReadOnlyError::class );
-               $noWriteService->duplicateAllAssociatedEntries(
-                       new TitleValue( 0, 'Foo' ),
-                       new TitleValue( 0, 'Bar' )
-               );
-       }
-
-}
diff --git a/tests/phpunit/languages/SpecialPageAliasTest.php b/tests/phpunit/languages/SpecialPageAliasTest.php
deleted file mode 100644 (file)
index d406c88..0000000
+++ /dev/null
@@ -1,64 +0,0 @@
-<?php
-
-/**
- * Verifies that special page aliases are valid, with no slashes.
- *
- * @group Language
- * @group SpecialPageAliases
- * @group SystemTest
- * @group medium
- * @todo This should be a structure test
- *
- * @author Katie Filbert < aude.wiki@gmail.com >
- */
-class SpecialPageAliasTest extends MediaWikiTestCase {
-
-       /**
-        * @coversNothing
-        * @dataProvider validSpecialPageAliasesProvider
-        */
-       public function testValidSpecialPageAliases( $code, $specialPageAliases ) {
-               foreach ( $specialPageAliases as $specialPage => $aliases ) {
-                       foreach ( $aliases as $alias ) {
-                               $msg = "$specialPage alias '$alias' in $code is valid with no slashes";
-                               $this->assertRegExp( '/^[^\/]*$/', $msg );
-                       }
-               }
-       }
-
-       public function validSpecialPageAliasesProvider() {
-               $codes = array_keys( Language::fetchLanguageNames( null, 'mwfile' ) );
-
-               $data = [];
-
-               foreach ( $codes as $code ) {
-                       $specialPageAliases = $this->getSpecialPageAliases( $code );
-
-                       if ( $specialPageAliases !== [] ) {
-                               $data[] = [ $code, $specialPageAliases ];
-                       }
-               }
-
-               return $data;
-       }
-
-       /**
-        * @param string $code
-        *
-        * @return array
-        */
-       protected function getSpecialPageAliases( $code ) {
-               $file = Language::getMessagesFileName( $code );
-
-               if ( is_readable( $file ) ) {
-                       include $file;
-
-                       if ( isset( $specialPageAliases ) && $specialPageAliases !== null ) {
-                               return $specialPageAliases;
-                       }
-               }
-
-               return [];
-       }
-
-}
diff --git a/tests/phpunit/structure/ApiPrefixUniquenessTest.php b/tests/phpunit/structure/ApiPrefixUniquenessTest.php
deleted file mode 100644 (file)
index 4329867..0000000
+++ /dev/null
@@ -1,48 +0,0 @@
-<?php
-
-/**
- * Checks that all API query modules, core and extensions, have unique prefixes.
- *
- * @group API
- */
-class ApiPrefixUniquenessTest extends MediaWikiTestCase {
-
-       public function testPrefixes() {
-               $main = new ApiMain( new FauxRequest() );
-               $query = new ApiQuery( $main, 'foo' );
-               $moduleManager = $query->getModuleManager();
-
-               $modules = $moduleManager->getNames();
-               $prefixes = [];
-
-               foreach ( $modules as $name ) {
-                       $module = $moduleManager->getModule( $name );
-                       $class = get_class( $module );
-
-                       $prefix = $module->getModulePrefix();
-                       if ( $prefix === '' /* HACK: T196962 */ || $prefix === 'wbeu' ) {
-                               continue;
-                       }
-
-                       if ( isset( $prefixes[$prefix] ) ) {
-                               $this->fail(
-                                       "Module prefix '{$prefix}' is shared between {$class} and {$prefixes[$prefix]}"
-                               );
-                       }
-                       $prefixes[$module->getModulePrefix()] = $class;
-
-                       if ( $module instanceof ApiQueryGeneratorBase ) {
-                               // namespace with 'g', a generator can share a prefix with a module
-                               $prefix = 'g' . $prefix;
-                               if ( isset( $prefixes[$prefix] ) ) {
-                                       $this->fail(
-                                               "Module prefix '{$prefix}' is shared between {$class} and {$prefixes[$prefix]}" .
-                                                       " (as a generator)"
-                                       );
-                               }
-                               $prefixes[$module->getModulePrefix()] = $class;
-                       }
-               }
-               $this->assertTrue( true ); // dummy call to make this test non-incomplete
-       }
-}
diff --git a/tests/phpunit/structure/AutoLoaderStructureTest.php b/tests/phpunit/structure/AutoLoaderStructureTest.php
deleted file mode 100644 (file)
index 37babce..0000000
+++ /dev/null
@@ -1,211 +0,0 @@
-<?php
-
-class AutoLoaderStructureTest extends MediaWikiTestCase {
-       /**
-        * Assert that there were no classes loaded that are not registered with the AutoLoader.
-        *
-        * For example foo.php having class Foo and class Bar but only registering Foo.
-        * This is important because we should not be relying on Foo being used before Bar.
-        */
-       public function testAutoLoadConfig() {
-               $results = self::checkAutoLoadConf();
-
-               $this->assertEquals(
-                       $results['expected'],
-                       $results['actual']
-               );
-       }
-
-       public function providePSR4Completeness() {
-               foreach ( AutoLoader::$psr4Namespaces as $prefix => $dir ) {
-                       foreach ( $this->recurseFiles( $dir ) as $file ) {
-                               yield [ $prefix, $dir, $file ];
-                       }
-               }
-       }
-
-       private function recurseFiles( $dir ) {
-               return ( new File_Iterator_Facade() )->getFilesAsArray( $dir, [ '.php' ] );
-       }
-
-       /**
-        * @dataProvider providePSR4Completeness
-        */
-       public function testPSR4Completeness( $prefix, $dir, $file ) {
-               global $wgAutoloadLocalClasses, $wgAutoloadClasses;
-               $contents = file_get_contents( $file );
-               list( $classesInFile, $aliasesInFile ) = self::parseFile( $contents );
-               $classes = array_keys( $classesInFile );
-               if ( $classes ) {
-                       $this->assertCount(
-                               1,
-                               $classes,
-                               "Only one class per file in PSR-4 autoloaded classes ($file)"
-                       );
-
-                       // Check that the expected class name (based on the filename) is the
-                       // same as the one we found.
-                       // Strip directory prefix from front of filename, and .php extension
-                       $dirNameLength = strlen( realpath( $dir ) ) + 1; // +1 for the trailing slash
-                       $fileBaseName = substr( $file, $dirNameLength );
-                       $abbrFileName = substr( $fileBaseName, 0, -4 );
-                       $expectedClassName = $prefix . str_replace( '/', '\\', $abbrFileName );
-
-                       $this->assertSame(
-                               $expectedClassName,
-                               $classes[0],
-                               "Class not autoloaded properly"
-                       );
-
-               } else {
-                       // Dummy assertion so this test isn't marked in risky
-                       // if the file has no classes nor aliases in it
-                       $this->assertCount( 0, $classes );
-               }
-
-               if ( $aliasesInFile ) {
-                       $otherClasses = $wgAutoloadLocalClasses + $wgAutoloadClasses;
-                       foreach ( $aliasesInFile as $alias => $class ) {
-                               $this->assertArrayHasKey( $alias, $otherClasses,
-                                       'Alias must be in the classmap autoloader'
-                               );
-                       }
-               }
-       }
-
-       private static function parseFile( $contents ) {
-               // We could use token_get_all() here, but this is faster
-               // Note: Keep in sync with ClassCollector
-               $matches = [];
-               preg_match_all( '/
-                               ^ [\t ]* (?:
-                                       (?:final\s+)? (?:abstract\s+)? (?:class|interface|trait) \s+
-                                       (?P<class> \w+)
-                               |
-                                       class_alias \s* \( \s*
-                                               ([\'"]) (?P<original> [^\'"]+) \g{-2} \s* , \s*
-                                               ([\'"]) (?P<alias> [^\'"]+ ) \g{-2} \s*
-                                       \) \s* ;
-                               |
-                                       class_alias \s* \( \s*
-                                               (?P<originalStatic> [\w\\\\]+)::class \s* , \s*
-                                               ([\'"]) (?P<aliasString> [^\'"]+ ) \g{-2} \s*
-                                       \) \s* ;
-                               )
-                       /imx', $contents, $matches, PREG_SET_ORDER );
-
-               $namespaceMatch = [];
-               preg_match( '/
-                               ^ [\t ]*
-                                       namespace \s+
-                                               (\w+(\\\\\w+)*)
-                                       \s* ;
-                       /imx', $contents, $namespaceMatch );
-               $fileNamespace = $namespaceMatch ? $namespaceMatch[1] . '\\' : '';
-
-               $classesInFile = [];
-               $aliasesInFile = [];
-
-               foreach ( $matches as $match ) {
-                       if ( !empty( $match['class'] ) ) {
-                               // 'class Foo {}'
-                               $class = $fileNamespace . $match['class'];
-                               $classesInFile[$class] = true;
-                       } elseif ( !empty( $match['original'] ) ) {
-                               // 'class_alias( "Foo", "Bar" );'
-                               $aliasesInFile[$match['alias']] = $match['original'];
-                       } else {
-                               // 'class_alias( Foo::class, "Bar" );'
-                               $aliasesInFile[$match['aliasString']] = $fileNamespace . $match['originalStatic'];
-                       }
-               }
-
-               return [ $classesInFile, $aliasesInFile ];
-       }
-
-       protected static function checkAutoLoadConf() {
-               global $wgAutoloadLocalClasses, $wgAutoloadClasses, $IP;
-
-               // wgAutoloadLocalClasses has precedence, just like in includes/AutoLoader.php
-               $expected = $wgAutoloadLocalClasses + $wgAutoloadClasses;
-               $actual = [];
-
-               $psr4Namespaces = [];
-               foreach ( AutoLoader::getAutoloadNamespaces() as $ns => $path ) {
-                       $psr4Namespaces[rtrim( $ns, '\\' ) . '\\'] = rtrim( $path, '/' );
-               }
-
-               foreach ( $expected as $class => $file ) {
-                       // Only prefix $IP if it doesn't have it already.
-                       // Generally local classes don't have it, and those from extensions and test suites do.
-                       if ( substr( $file, 0, 1 ) != '/' && substr( $file, 1, 1 ) != ':' ) {
-                               $filePath = "$IP/$file";
-                       } else {
-                               $filePath = $file;
-                       }
-
-                       if ( !file_exists( $filePath ) ) {
-                               $actual[$class] = "[file '$filePath' does not exist]";
-                               continue;
-                       }
-
-                       Wikimedia\suppressWarnings();
-                       $contents = file_get_contents( $filePath );
-                       Wikimedia\restoreWarnings();
-
-                       if ( $contents === false ) {
-                               $actual[$class] = "[couldn't read file '$filePath']";
-                               continue;
-                       }
-
-                       list( $classesInFile, $aliasesInFile ) = self::parseFile( $contents );
-
-                       foreach ( $classesInFile as $className => $ignore ) {
-                               // Skip if it's a PSR4 class
-                               $parts = explode( '\\', $className );
-                               for ( $i = count( $parts ) - 1; $i > 0; $i-- ) {
-                                       $ns = implode( '\\', array_slice( $parts, 0, $i ) ) . '\\';
-                                       if ( isset( $psr4Namespaces[$ns] ) ) {
-                                               $expectedPath = $psr4Namespaces[$ns] . '/'
-                                                       . implode( '/', array_slice( $parts, $i ) )
-                                                       . '.php';
-                                               if ( $filePath === $expectedPath ) {
-                                                       continue 2;
-                                               }
-                                       }
-                               }
-
-                               // Nope, add it.
-                               $actual[$className] = $file;
-                       }
-
-                       // Only accept aliases for classes in the same file, because for correct
-                       // behavior, all aliases for a class must be set up when the class is loaded
-                       // (see <https://bugs.php.net/bug.php?id=61422>).
-                       foreach ( $aliasesInFile as $alias => $class ) {
-                               if ( isset( $classesInFile[$class] ) ) {
-                                       $actual[$alias] = $file;
-                               } else {
-                                       $actual[$alias] = "[original class not in $file]";
-                               }
-                       }
-               }
-
-               return [
-                       'expected' => $expected,
-                       'actual' => $actual,
-               ];
-       }
-
-       public function testAutoloadOrder() {
-               $path = realpath( __DIR__ . '/../../..' );
-               $oldAutoload = file_get_contents( $path . '/autoload.php' );
-               $generator = new AutoloadGenerator( $path, 'local' );
-               $generator->setPsr4Namespaces( AutoLoader::getAutoloadNamespaces() );
-               $generator->initMediaWikiDefault();
-               $newAutoload = $generator->getAutoload( 'maintenance/generateLocalAutoload.php' );
-
-               $this->assertEquals( $oldAutoload, $newAutoload, 'autoload.php does not match' .
-                       ' output of generateLocalAutoload.php script.' );
-       }
-}
diff --git a/tests/phpunit/structure/ContentHandlerSanityTest.php b/tests/phpunit/structure/ContentHandlerSanityTest.php
deleted file mode 100644 (file)
index c8bcd60..0000000
+++ /dev/null
@@ -1,59 +0,0 @@
-<?php
-
-use Wikimedia\TestingAccessWrapper;
-
-/**
- * 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; either version 2 of the License, or
- * (at your option) any later version.
- *
- * 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.
- * http://www.gnu.org/copyleft/gpl.html
- */
-
-class ContentHandlerSanityTest extends MediaWikiTestCase {
-
-       public static function provideHandlers() {
-               $models = ContentHandler::getContentModels();
-               $handlers = [];
-               foreach ( $models as $model ) {
-                       $handlers[] = [ ContentHandler::getForModelID( $model ) ];
-               }
-
-               return $handlers;
-       }
-
-       /**
-        * @dataProvider provideHandlers
-        * @param ContentHandler $handler
-        */
-       public function testMakeEmptyContent( ContentHandler $handler ) {
-               $content = $handler->makeEmptyContent();
-               $this->assertInstanceOf( Content::class, $content );
-               if ( $handler instanceof TextContentHandler ) {
-                       // TextContentHandler::getContentClass() is protected, so bypass
-                       // that restriction
-                       $testingWrapper = TestingAccessWrapper::newFromObject( $handler );
-                       $this->assertInstanceOf( $testingWrapper->getContentClass(), $content );
-               }
-
-               $handlerClass = get_class( $handler );
-               $contentClass = get_class( $content );
-
-               if ( $handler->supportsDirectEditing() ) {
-                       $this->assertTrue(
-                               $content->isValid(),
-                               "$handlerClass::makeEmptyContent() did not return a valid content ($contentClass::isValid())"
-                       );
-               }
-       }
-
-}
diff --git a/tests/phpunit/structure/PasswordPolicyStructureTest.php b/tests/phpunit/structure/PasswordPolicyStructureTest.php
deleted file mode 100644 (file)
index d7f865d..0000000
+++ /dev/null
@@ -1,46 +0,0 @@
-<?php
-
-class PasswordPolicyStructureTest extends MediaWikiTestCase {
-
-       public function provideChecks() {
-               global $wgPasswordPolicy;
-
-               foreach ( $wgPasswordPolicy['checks'] as $name => $callback ) {
-                       yield [ $name ];
-               }
-       }
-
-       public function provideFlags() {
-               global $wgPasswordPolicy;
-
-               // This won't actually find all flags, just the ones in use. Can't really be helped,
-               // other than adding the core flags here.
-               $flags = [ 'forceChange', 'suggestChangeOnLogin' ];
-               foreach ( $wgPasswordPolicy['policies'] as $group => $checks ) {
-                       foreach ( $checks as $check => $settings ) {
-                               if ( is_array( $settings ) ) {
-                                       $flags = array_unique(
-                                               array_merge( $flags, array_diff( array_keys( $settings ), [ 'value' ] ) )
-                                       );
-                               }
-                       }
-               }
-
-               foreach ( $flags as $flag ) {
-                       yield [ $flag ];
-               }
-       }
-
-       /** @dataProvider provideChecks */
-       public function testCheckMessage( $check ) {
-               $msg = wfMessage( 'passwordpolicies-policy-' . strtolower( $check ) );
-               $this->assertTrue( $msg->exists() );
-       }
-
-       /** @dataProvider provideFlags */
-       public function testFlagMessage( $flag ) {
-               $msg = wfMessage( 'passwordpolicies-policyflag-' . strtolower( $flag ) );
-               $this->assertTrue( $msg->exists() );
-       }
-
-}
index 412ee99..45eb216 100644 (file)
@@ -21,6 +21,7 @@ class StructureTest extends MediaWikiTestCase {
                        'ApiQueryContinueTestBase',
                        'MediaWikiLangTestCase',
                        'MediaWikiMediaTestCase',
+                       'MediaWikiUnitTestCase',
                        'MediaWikiTestCase',
                        'ResourceLoaderTestCase',
                        'PHPUnit_Framework_TestCase',
index de68fec..6bec661 100644 (file)
                <testsuite name="documentation">
                        <directory>documentation</directory>
                </testsuite>
+
+               <!-- Unit tests separation -->
+               <testsuite name="unit_includes">
+                       <directory>unit/includes</directory>
+               </testsuite>
+               <testsuite name="unit_languages">
+                       <directory>unit/languages</directory>
+               </testsuite>
+               <testsuite name="unit_structure">
+                       <directory>unit/structure</directory>
+               </testsuite>
        </testsuites>
        <groups>
                <exclude>
diff --git a/tests/phpunit/unit-tests.xml b/tests/phpunit/unit-tests.xml
new file mode 100644 (file)
index 0000000..149f1f2
--- /dev/null
@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<phpunit bootstrap="unit/initUnitTests.php"
+                xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+                xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/4.8/phpunit.xsd"
+                colors="true"
+                backupGlobals="false"
+                convertErrorsToExceptions="true"
+                convertNoticesToExceptions="true"
+                convertWarningsToExceptions="true"
+                forceCoversAnnotation="true"
+                stopOnFailure="false"
+                timeoutForSmallTests="10"
+                timeoutForMediumTests="30"
+                timeoutForLargeTests="60"
+                beStrictAboutTestsThatDoNotTestAnything="true"
+                beStrictAboutOutputDuringTests="true"
+                beStrictAboutTestSize="true"
+                verbose="false">
+       <testsuites>
+               <testsuite name="includes">
+                       <directory>unit/includes</directory>
+               </testsuite>
+               <testsuite name="languages">
+                       <directory>unit/languages</directory>
+               </testsuite>
+               <testsuite name="structure">
+                       <directory>unit/structure</directory>
+               </testsuite>
+       </testsuites>
+       <groups>
+               <exclude>
+                       <group>Utility</group>
+                       <group>Broken</group>
+                       <group>Stub</group>
+               </exclude>
+       </groups>
+       <filter>
+               <whitelist addUncoveredFilesFromWhitelist="true">
+                       <directory suffix=".php">../../includes</directory>
+                       <directory suffix=".php">../../languages</directory>
+                       <directory suffix=".php">../../maintenance</directory>
+                       <exclude>
+                               <directory suffix=".php">../../languages/messages</directory>
+                               <file>../../languages/data/normalize-ar.php</file>
+                               <file>../../languages/data/normalize-ml.php</file>
+                       </exclude>
+               </whitelist>
+       </filter>
+</phpunit>
diff --git a/tests/phpunit/unit/documentation/ReleaseNotesTest.php b/tests/phpunit/unit/documentation/ReleaseNotesTest.php
new file mode 100644 (file)
index 0000000..701cb56
--- /dev/null
@@ -0,0 +1,69 @@
+<?php
+
+/**
+ * James doesn't like having to manually fix these things.
+ */
+class ReleaseNotesTest extends \MediaWikiUnitTestCase {
+       /**
+        * Verify that at least one Release Notes file exists, have content, and
+        * aren't overly long.
+        *
+        * @group documentation
+        * @coversNothing
+        */
+       public function testReleaseNotesFilesExistAndAreNotMalformed() {
+               global $wgVersion, $IP;
+
+               $notesFiles = glob( "$IP/RELEASE-NOTES-*" );
+
+               $this->assertGreaterThanOrEqual(
+                       1,
+                       count( $notesFiles ),
+                       'Repo has at least one Release Notes file.'
+               );
+
+               $versionParts = explode( '.', explode( '-', $wgVersion )[0] );
+               $this->assertContains(
+                       "$IP/RELEASE-NOTES-$versionParts[0].$versionParts[1]",
+                       $notesFiles,
+                       'Repo has a Release Notes file for the current $wgVersion.'
+               );
+
+               foreach ( $notesFiles as $index => $fileName ) {
+                       $this->assertFileLength( "Release Notes", $fileName );
+               }
+
+               // Also test the README and similar files
+               $otherFiles = [
+                       "$IP/COPYING",
+                       "$IP/FAQ",
+                       "$IP/HISTORY",
+                       "$IP/INSTALL",
+                       "$IP/README",
+                       "$IP/SECURITY"
+               ];
+
+               foreach ( $otherFiles as $index => $fileName ) {
+                       $this->assertFileLength( "Help", $fileName );
+               }
+       }
+
+       private function assertFileLength( $type, $fileName ) {
+               $file = file( $fileName, FILE_IGNORE_NEW_LINES );
+
+               $this->assertFalse(
+                       !$file,
+                       "$type file '$fileName' is inaccessible."
+               );
+
+               foreach ( $file as $i => $line ) {
+                       $num = $i + 1;
+                       $this->assertLessThanOrEqual(
+                               // FILE_IGNORE_NEW_LINES drops the \n at the EOL, so max length is 80 not 81.
+                               80,
+                               mb_strlen( $line ),
+                               "$type file '$fileName' line $num, is longer than 80 chars:\n\t'$line'"
+                       );
+               }
+       }
+}
diff --git a/tests/phpunit/unit/includes/CommentStoreCommentTest.php b/tests/phpunit/unit/includes/CommentStoreCommentTest.php
new file mode 100644 (file)
index 0000000..2dfe03a
--- /dev/null
@@ -0,0 +1,26 @@
+<?php
+
+use PHPUnit\Framework\TestCase;
+
+/**
+ * @covers CommentStoreComment
+ *
+ * @license GPL-2.0-or-later
+ */
+class CommentStoreCommentTest extends TestCase {
+
+       public function testConstructorWithMessage() {
+               $message = new Message( 'test' );
+               $comment = new CommentStoreComment( null, 'test', $message );
+
+               $this->assertSame( $message, $comment->message );
+       }
+
+       public function testConstructorWithoutMessage() {
+               $text = '{{template|param}}';
+               $comment = new CommentStoreComment( null, $text );
+
+               $this->assertSame( $text, $comment->message->text() );
+       }
+
+}
diff --git a/tests/phpunit/unit/includes/DerivativeRequestTest.php b/tests/phpunit/unit/includes/DerivativeRequestTest.php
new file mode 100644 (file)
index 0000000..f33022b
--- /dev/null
@@ -0,0 +1,21 @@
+<?php
+
+/**
+ * @covers DerivativeRequest
+ */
+class DerivativeRequestTest extends PHPUnit\Framework\TestCase {
+
+       public function testSetIp() {
+               $original = new WebRequest();
+               $original->setIP( '1.2.3.4' );
+               $derivative = new DerivativeRequest( $original, [] );
+
+               $this->assertEquals( '1.2.3.4', $derivative->getIP() );
+
+               $derivative->setIP( '5.6.7.8' );
+
+               $this->assertEquals( '5.6.7.8', $derivative->getIP() );
+               $this->assertEquals( '1.2.3.4', $original->getIP() );
+       }
+
+}
diff --git a/tests/phpunit/unit/includes/FauxRequestTest.php b/tests/phpunit/unit/includes/FauxRequestTest.php
new file mode 100644 (file)
index 0000000..c054caa
--- /dev/null
@@ -0,0 +1,294 @@
+<?php
+
+use MediaWiki\Session\SessionManager;
+
+class FauxRequestTest extends PHPUnit\Framework\TestCase {
+
+       use MediaWikiCoversValidator;
+       use PHPUnit4And6Compat;
+
+       public function setUp() {
+               parent::setUp();
+               $this->orgWgServer = $GLOBALS['wgServer'];
+       }
+
+       public function tearDown() {
+               $GLOBALS['wgServer'] = $this->orgWgServer;
+               parent::tearDown();
+       }
+
+       /**
+        * @covers FauxRequest::__construct
+        */
+       public function testConstructInvalidData() {
+               $this->setExpectedException( MWException::class, 'bogus data' );
+               $req = new FauxRequest( 'x' );
+       }
+
+       /**
+        * @covers FauxRequest::__construct
+        */
+       public function testConstructInvalidSession() {
+               $this->setExpectedException( MWException::class, 'bogus session' );
+               $req = new FauxRequest( [], false, 'x' );
+       }
+
+       /**
+        * @covers FauxRequest::__construct
+        */
+       public function testConstructWithSession() {
+               $session = SessionManager::singleton()->getEmptySession( new FauxRequest( [] ) );
+               $this->assertInstanceOf(
+                       FauxRequest::class,
+                       new FauxRequest( [], false, $session )
+               );
+       }
+
+       /**
+        * @covers FauxRequest::getText
+        */
+       public function testGetText() {
+               $req = new FauxRequest( [ 'x' => 'Value' ] );
+               $this->assertEquals( 'Value', $req->getText( 'x' ) );
+               $this->assertEquals( '', $req->getText( 'z' ) );
+       }
+
+       /**
+        * Integration test for parent method
+        * @covers FauxRequest::getVal
+        */
+       public function testGetVal() {
+               $req = new FauxRequest( [ 'crlf' => "A\r\nb" ] );
+               $this->assertSame( "A\r\nb", $req->getVal( 'crlf' ), 'CRLF' );
+       }
+
+       /**
+        * Integration test for parent method
+        * @covers FauxRequest::getRawVal
+        */
+       public function testGetRawVal() {
+               $req = new FauxRequest( [
+                       'x' => 'Value',
+                       'y' => [ 'a' ],
+                       'crlf' => "A\r\nb"
+               ] );
+               $this->assertSame( 'Value', $req->getRawVal( 'x' ) );
+               $this->assertSame( null, $req->getRawVal( 'z' ), 'Not found' );
+               $this->assertSame( null, $req->getRawVal( 'y' ), 'Array is ignored' );
+               $this->assertSame( "A\r\nb", $req->getRawVal( 'crlf' ), 'CRLF' );
+       }
+
+       /**
+        * @covers FauxRequest::getValues
+        */
+       public function testGetValues() {
+               $values = [ 'x' => 'Value', 'y' => '' ];
+               $req = new FauxRequest( $values );
+               $this->assertEquals( $values, $req->getValues() );
+       }
+
+       /**
+        * @covers FauxRequest::getQueryValues
+        */
+       public function testGetQueryValues() {
+               $values = [ 'x' => 'Value', 'y' => '' ];
+
+               $req = new FauxRequest( $values );
+               $this->assertEquals( $values, $req->getQueryValues() );
+               $req = new FauxRequest( $values, /*wasPosted*/ true );
+               $this->assertEquals( [], $req->getQueryValues() );
+       }
+
+       /**
+        * @covers FauxRequest::getMethod
+        */
+       public function testGetMethod() {
+               $req = new FauxRequest( [] );
+               $this->assertEquals( 'GET', $req->getMethod() );
+               $req = new FauxRequest( [], /*wasPosted*/ true );
+               $this->assertEquals( 'POST', $req->getMethod() );
+       }
+
+       /**
+        * @covers FauxRequest::wasPosted
+        */
+       public function testWasPosted() {
+               $req = new FauxRequest( [] );
+               $this->assertFalse( $req->wasPosted() );
+               $req = new FauxRequest( [], /*wasPosted*/ true );
+               $this->assertTrue( $req->wasPosted() );
+       }
+
+       /**
+        * @covers FauxRequest::getCookie
+        * @covers FauxRequest::setCookie
+        * @covers FauxRequest::setCookies
+        */
+       public function testCookies() {
+               $req = new FauxRequest();
+               $this->assertSame( null, $req->getCookie( 'z', '' ) );
+
+               $req->setCookie( 'x', 'Value', '' );
+               $this->assertEquals( 'Value', $req->getCookie( 'x', '' ) );
+
+               $req->setCookies( [ 'x' => 'One', 'y' => 'Two' ], '' );
+               $this->assertEquals( 'One', $req->getCookie( 'x', '' ) );
+               $this->assertEquals( 'Two', $req->getCookie( 'y', '' ) );
+       }
+
+       /**
+        * @covers FauxRequest::getCookie
+        * @covers FauxRequest::setCookie
+        * @covers FauxRequest::setCookies
+        */
+       public function testCookiesDefaultPrefix() {
+               global $wgCookiePrefix;
+               $oldPrefix = $wgCookiePrefix;
+               $wgCookiePrefix = '_';
+
+               $req = new FauxRequest();
+               $this->assertSame( null, $req->getCookie( 'z' ) );
+
+               $req->setCookie( 'x', 'Value' );
+               $this->assertEquals( 'Value', $req->getCookie( 'x' ) );
+
+               $wgCookiePrefix = $oldPrefix;
+       }
+
+       /**
+        * @covers FauxRequest::getRequestURL
+        */
+       public function testGetRequestURL_disallowed() {
+               $req = new FauxRequest();
+               $this->setExpectedException( MWException::class );
+               $req->getRequestURL();
+       }
+
+       /**
+        * @covers FauxRequest::setRequestURL
+        * @covers FauxRequest::getRequestURL
+        */
+       public function testSetRequestURL() {
+               $req = new FauxRequest();
+               $req->setRequestURL( 'https://example.org' );
+               $this->assertEquals( 'https://example.org', $req->getRequestURL() );
+       }
+
+       /**
+        * @covers FauxRequest::getFullRequestURL
+        */
+       public function testGetFullRequestURL_disallowed() {
+               $GLOBALS['wgServer'] = '//wiki.test';
+               $req = new FauxRequest();
+
+               $this->setExpectedException( MWException::class );
+               $req->getFullRequestURL();
+       }
+
+       /**
+        * @covers FauxRequest::getFullRequestURL
+        */
+       public function testGetFullRequestURL_http() {
+               $GLOBALS['wgServer'] = '//wiki.test';
+               $req = new FauxRequest();
+               $req->setRequestURL( '/path' );
+
+               $this->assertSame(
+                       'http://wiki.test/path',
+                       $req->getFullRequestURL()
+               );
+       }
+
+       /**
+        * @covers FauxRequest::getFullRequestURL
+        */
+       public function testGetFullRequestURL_https() {
+               $GLOBALS['wgServer'] = '//wiki.test';
+               $req = new FauxRequest( [], false, null, 'https' );
+               $req->setRequestURL( '/path' );
+
+               $this->assertSame(
+                       'https://wiki.test/path',
+                       $req->getFullRequestURL()
+               );
+       }
+
+       /**
+        * @covers FauxRequest::__construct
+        * @covers FauxRequest::getProtocol
+        */
+       public function testProtocol() {
+               $req = new FauxRequest();
+               $this->assertEquals( 'http', $req->getProtocol() );
+               $req = new FauxRequest( [], false, null, 'http' );
+               $this->assertEquals( 'http', $req->getProtocol() );
+               $req = new FauxRequest( [], false, null, 'https' );
+               $this->assertEquals( 'https', $req->getProtocol() );
+       }
+
+       /**
+        * @covers FauxRequest::setHeader
+        * @covers FauxRequest::setHeaders
+        * @covers FauxRequest::getHeader
+        */
+       public function testGetSetHeader() {
+               $value = 'text/plain, text/html';
+
+               $request = new FauxRequest();
+               $request->setHeader( 'Accept', $value );
+
+               $this->assertEquals( $request->getHeader( 'Nonexistent' ), false );
+               $this->assertEquals( $request->getHeader( 'Accept' ), $value );
+               $this->assertEquals( $request->getHeader( 'ACCEPT' ), $value );
+               $this->assertEquals( $request->getHeader( 'accept' ), $value );
+               $this->assertEquals(
+                       $request->getHeader( 'Accept', WebRequest::GETHEADER_LIST ),
+                       [ 'text/plain', 'text/html' ]
+               );
+       }
+
+       /**
+        * @covers FauxRequest::initHeaders
+        */
+       public function testGetAllHeaders() {
+               $_SERVER['HTTP_TEST'] = 'Example';
+
+               $request = new FauxRequest();
+
+               $this->assertEquals(
+                       [],
+                       $request->getAllHeaders()
+               );
+
+               $this->assertEquals(
+                       false,
+                       $request->getHeader( 'test' )
+               );
+       }
+
+       /**
+        * @covers FauxRequest::__construct
+        * @covers FauxRequest::getSessionArray
+        */
+       public function testSessionData() {
+               $values = [ 'x' => 'Value', 'y' => '' ];
+
+               $req = new FauxRequest( [], false, /*session*/ $values );
+               $this->assertEquals( $values, $req->getSessionArray() );
+
+               $req = new FauxRequest();
+               $this->assertSame( null, $req->getSessionArray() );
+       }
+
+       /**
+        * @covers FauxRequest::getRawQueryString
+        * @covers FauxRequest::getRawPostString
+        * @covers FauxRequest::getRawInput
+        */
+       public function testDummies() {
+               $req = new FauxRequest();
+               $this->assertEquals( '', $req->getRawQueryString() );
+               $this->assertEquals( '', $req->getRawPostString() );
+               $this->assertEquals( '', $req->getRawInput() );
+       }
+}
diff --git a/tests/phpunit/unit/includes/FauxResponseTest.php b/tests/phpunit/unit/includes/FauxResponseTest.php
new file mode 100644 (file)
index 0000000..5e208ac
--- /dev/null
@@ -0,0 +1,146 @@
+<?php
+/**
+ * Copyright @ 2011 Alexandre Emsenhuber
+ *
+ * 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; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * 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.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+class FauxResponseTest extends \MediaWikiUnitTestCase {
+       /** @var FauxResponse */
+       protected $response;
+
+       protected function setUp() {
+               parent::setUp();
+               $this->response = new FauxResponse;
+       }
+
+       /**
+        * @covers FauxResponse::setCookie
+        * @covers FauxResponse::getCookie
+        * @covers FauxResponse::getCookieData
+        * @covers FauxResponse::getCookies
+        */
+       public function testCookie() {
+               $expire = time() + 100;
+               $cookie = [
+                       'value' => 'val',
+                       'path' => '/path',
+                       'domain' => 'domain',
+                       'secure' => true,
+                       'httpOnly' => false,
+                       'raw' => false,
+                       'expire' => $expire,
+               ];
+
+               $this->assertEquals( null, $this->response->getCookie( 'xkey' ), 'Non-existing cookie' );
+               $this->response->setCookie( 'key', 'val', $expire, [
+                       'prefix' => 'x',
+                       'path' => '/path',
+                       'domain' => 'domain',
+                       'secure' => 1,
+                       'httpOnly' => 0,
+               ] );
+               $this->assertEquals( 'val', $this->response->getCookie( 'xkey' ), 'Existing cookie' );
+               $this->assertEquals( $cookie, $this->response->getCookieData( 'xkey' ),
+                       'Existing cookie (data)' );
+               $this->assertEquals( [ 'xkey' => $cookie ], $this->response->getCookies(),
+                       'Existing cookies' );
+       }
+
+       /**
+        * @covers FauxResponse::getheader
+        * @covers FauxResponse::header
+        */
+       public function testHeader() {
+               $this->assertEquals( null, $this->response->getHeader( 'Location' ), 'Non-existing header' );
+
+               $this->response->header( 'Location: http://localhost/' );
+               $this->assertEquals(
+                       'http://localhost/',
+                       $this->response->getHeader( 'Location' ),
+                       'Set header'
+               );
+
+               $this->response->header( 'Location: http://127.0.0.1/' );
+               $this->assertEquals(
+                       'http://127.0.0.1/',
+                       $this->response->getHeader( 'Location' ),
+                       'Same header'
+               );
+
+               $this->response->header( 'Location: http://127.0.0.2/', false );
+               $this->assertEquals(
+                       'http://127.0.0.1/',
+                       $this->response->getHeader( 'Location' ),
+                       'Same header with override disabled'
+               );
+
+               $this->response->header( 'Location: http://localhost/' );
+               $this->assertEquals(
+                       'http://localhost/',
+                       $this->response->getHeader( 'LOCATION' ),
+                       'Get header case insensitive'
+               );
+       }
+
+       /**
+        * @covers FauxResponse::getStatusCode
+        */
+       public function testResponseCode() {
+               $this->response->header( 'HTTP/1.1 200' );
+               $this->assertEquals( 200, $this->response->getStatusCode(), 'Header with no message' );
+
+               $this->response->header( 'HTTP/1.x 201' );
+               $this->assertEquals(
+                       201,
+                       $this->response->getStatusCode(),
+                       'Header with no message and protocol 1.x'
+               );
+
+               $this->response->header( 'HTTP/1.1 202 OK' );
+               $this->assertEquals( 202, $this->response->getStatusCode(), 'Normal header' );
+
+               $this->response->header( 'HTTP/1.x 203 OK' );
+               $this->assertEquals(
+                       203,
+                       $this->response->getStatusCode(),
+                       'Normal header with no message and protocol 1.x'
+               );
+
+               $this->response->header( 'HTTP/1.x 204 OK', false, 205 );
+               $this->assertEquals(
+                       205,
+                       $this->response->getStatusCode(),
+                       'Third parameter overrides the HTTP/... header'
+               );
+
+               $this->response->statusHeader( 210 );
+               $this->assertEquals(
+                       210,
+                       $this->response->getStatusCode(),
+                       'Handle statusHeader method'
+               );
+
+               $this->response->header( 'Location: http://localhost/', false, 206 );
+               $this->assertEquals(
+                       206,
+                       $this->response->getStatusCode(),
+                       'Third parameter with another header'
+               );
+       }
+}
diff --git a/tests/phpunit/unit/includes/FormOptionsInitializationTest.php b/tests/phpunit/unit/includes/FormOptionsInitializationTest.php
new file mode 100644 (file)
index 0000000..708956d
--- /dev/null
@@ -0,0 +1,70 @@
+<?php
+
+use Wikimedia\TestingAccessWrapper;
+
+/**
+ * Test class for FormOptions initialization
+ * Ensure the FormOptions::add() does what we want it to do.
+ *
+ * Copyright © 2011, Antoine Musso
+ *
+ * @author Antoine Musso
+ */
+class FormOptionsInitializationTest extends \MediaWikiUnitTestCase {
+       /**
+        * @var FormOptions
+        */
+       protected $object;
+
+       /**
+        * A new fresh and empty FormOptions object to test initialization
+        * with.
+        */
+       protected function setUp() {
+               parent::setUp();
+               $this->object = TestingAccessWrapper::newFromObject( new FormOptions() );
+       }
+
+       /**
+        * @covers FormOptions::add
+        */
+       public function testAddStringOption() {
+               $this->object->add( 'foo', 'string value' );
+               $this->assertEquals(
+                       [
+                               'foo' => [
+                                       'default' => 'string value',
+                                       'consumed' => false,
+                                       'type' => FormOptions::STRING,
+                                       'value' => null,
+                               ]
+                       ],
+                       $this->object->options
+               );
+       }
+
+       /**
+        * @covers FormOptions::add
+        */
+       public function testAddIntegers() {
+               $this->object->add( 'one', 1 );
+               $this->object->add( 'negone', -1 );
+               $this->assertEquals(
+                       [
+                               'negone' => [
+                                       'default' => -1,
+                                       'value' => null,
+                                       'consumed' => false,
+                                       'type' => FormOptions::INT,
+                               ],
+                               'one' => [
+                                       'default' => 1,
+                                       'value' => null,
+                                       'consumed' => false,
+                                       'type' => FormOptions::INT,
+                               ]
+                       ],
+                       $this->object->options
+               );
+       }
+}
diff --git a/tests/phpunit/unit/includes/FormOptionsTest.php b/tests/phpunit/unit/includes/FormOptionsTest.php
new file mode 100644 (file)
index 0000000..c14595b
--- /dev/null
@@ -0,0 +1,105 @@
+<?php
+/**
+ * This file host two test case classes for the MediaWiki FormOptions class:
+ *  - FormOptionsInitializationTest : tests initialization of the class.
+ *  - FormOptionsTest : tests methods an on instance
+ *
+ * The split let us take advantage of setting up a fixture for the methods
+ * tests.
+ */
+
+/**
+ * Test class for FormOptions methods.
+ *
+ * Copyright © 2011, Antoine Musso
+ *
+ * @author Antoine Musso
+ */
+class FormOptionsTest extends \MediaWikiUnitTestCase {
+       /**
+        * @var FormOptions
+        */
+       protected $object;
+
+       /**
+        * Instanciates a FormOptions object to play with.
+        * FormOptions::add() is tested by the class FormOptionsInitializationTest
+        * so we assume the function is well tested already an use it to create
+        * the fixture.
+        */
+       protected function setUp() {
+               parent::setUp();
+               $this->object = new FormOptions;
+               $this->object->add( 'string1', 'string one' );
+               $this->object->add( 'string2', 'string two' );
+               $this->object->add( 'integer', 0 );
+               $this->object->add( 'float', 0.0 );
+               $this->object->add( 'intnull', 0, FormOptions::INTNULL );
+       }
+
+       /** Helpers for testGuessType() */
+       /* @{ */
+       private function assertGuessBoolean( $data ) {
+               $this->guess( FormOptions::BOOL, $data );
+       }
+
+       private function assertGuessInt( $data ) {
+               $this->guess( FormOptions::INT, $data );
+       }
+
+       private function assertGuessFloat( $data ) {
+               $this->guess( FormOptions::FLOAT, $data );
+       }
+
+       private function assertGuessString( $data ) {
+               $this->guess( FormOptions::STRING, $data );
+       }
+
+       private function assertGuessArray( $data ) {
+               $this->guess( FormOptions::ARR, $data );
+       }
+
+       /** Generic helper */
+       private function guess( $expected, $data ) {
+               $this->assertEquals(
+                       $expected,
+                       FormOptions::guessType( $data )
+               );
+       }
+
+       /* @} */
+
+       /**
+        * Reuse helpers above assertGuessBoolean assertGuessInt assertGuessString
+        * @covers FormOptions::guessType
+        */
+       public function testGuessTypeDetection() {
+               $this->assertGuessBoolean( true );
+               $this->assertGuessBoolean( false );
+
+               $this->assertGuessInt( 0 );
+               $this->assertGuessInt( -5 );
+               $this->assertGuessInt( 5 );
+               $this->assertGuessInt( 0x0F );
+
+               $this->assertGuessFloat( 0.0 );
+               $this->assertGuessFloat( 1.5 );
+               $this->assertGuessFloat( 1e3 );
+
+               $this->assertGuessString( 'true' );
+               $this->assertGuessString( 'false' );
+               $this->assertGuessString( '5' );
+               $this->assertGuessString( '0' );
+               $this->assertGuessString( '1.5' );
+
+               $this->assertGuessArray( [ 'foo' ] );
+       }
+
+       /**
+        * @expectedException MWException
+        * @covers FormOptions::guessType
+        */
+       public function testGuessTypeOnNullThrowException() {
+               $this->object->guessType( null );
+       }
+}
diff --git a/tests/phpunit/unit/includes/GlobalFunctions/wfAppendQueryTest.php b/tests/phpunit/unit/includes/GlobalFunctions/wfAppendQueryTest.php
new file mode 100644 (file)
index 0000000..27ac239
--- /dev/null
@@ -0,0 +1,79 @@
+<?php
+
+/**
+ * @group GlobalFunctions
+ * @covers ::wfAppendQuery
+ */
+class WfAppendQueryTest extends \MediaWikiUnitTestCase {
+       /**
+        * @dataProvider provideAppendQuery
+        */
+       public function testAppendQuery( $url, $query, $expected, $message = null ) {
+               $this->assertEquals( $expected, wfAppendQuery( $url, $query ), $message );
+       }
+
+       public static function provideAppendQuery() {
+               return [
+                       [
+                               'http://www.example.org/index.php',
+                               '',
+                               'http://www.example.org/index.php',
+                               'No query'
+                       ],
+                       [
+                               'http://www.example.org/index.php',
+                               [ 'foo' => 'bar' ],
+                               'http://www.example.org/index.php?foo=bar',
+                               'Set query array'
+                       ],
+                       [
+                               'http://www.example.org/index.php?foz=baz',
+                               'foo=bar',
+                               'http://www.example.org/index.php?foz=baz&foo=bar',
+                               'Set query string'
+                       ],
+                       [
+                               'http://www.example.org/index.php?foo=bar',
+                               '',
+                               'http://www.example.org/index.php?foo=bar',
+                               'Empty string with query'
+                       ],
+                       [
+                               'http://www.example.org/index.php?foo=bar',
+                               [ 'baz' => 'quux' ],
+                               'http://www.example.org/index.php?foo=bar&baz=quux',
+                               'Add query array'
+                       ],
+                       [
+                               'http://www.example.org/index.php?foo=bar',
+                               'baz=quux',
+                               'http://www.example.org/index.php?foo=bar&baz=quux',
+                               'Add query string'
+                       ],
+                       [
+                               'http://www.example.org/index.php?foo=bar',
+                               [ 'baz' => 'quux', 'foo' => 'baz' ],
+                               'http://www.example.org/index.php?foo=bar&baz=quux&foo=baz',
+                               'Modify query array'
+                       ],
+                       [
+                               'http://www.example.org/index.php?foo=bar',
+                               'baz=quux&foo=baz',
+                               'http://www.example.org/index.php?foo=bar&baz=quux&foo=baz',
+                               'Modify query string'
+                       ],
+                       [
+                               'http://www.example.org/index.php#baz',
+                               'foo=bar',
+                               'http://www.example.org/index.php?foo=bar#baz',
+                               'URL with fragment'
+                       ],
+                       [
+                               'http://www.example.org/index.php?foo=bar#baz',
+                               'quux=blah',
+                               'http://www.example.org/index.php?foo=bar&quux=blah#baz',
+                               'URL with query string and fragment'
+                       ]
+               ];
+       }
+}
diff --git a/tests/phpunit/unit/includes/GlobalFunctions/wfArrayPlus2dTest.php b/tests/phpunit/unit/includes/GlobalFunctions/wfArrayPlus2dTest.php
new file mode 100644 (file)
index 0000000..3e65af5
--- /dev/null
@@ -0,0 +1,94 @@
+<?php
+/**
+ * @group GlobalFunctions
+ * @covers ::wfArrayPlus2d
+ */
+class WfArrayPlus2dTest extends \MediaWikiUnitTestCase {
+       /**
+        * @dataProvider provideArrays
+        */
+       public function testWfArrayPlus2d( $baseArray, $newValues, $expected, $testName ) {
+               $this->assertEquals(
+                       $expected,
+                       wfArrayPlus2d( $baseArray, $newValues ),
+                       $testName
+               );
+       }
+
+       /**
+        * Provider for testing wfArrayPlus2d
+        *
+        * @return array
+        */
+       public static function provideArrays() {
+               return [
+                       // target array, new values array, expected result
+                       [
+                               [ 0 => '1dArray' ],
+                               [ 1 => '1dArray' ],
+                               [ 0 => '1dArray', 1 => '1dArray' ],
+                               "Test simple union of two arrays with different keys",
+                       ],
+                       [
+                               [
+                                       0 => [ 0 => '2dArray' ],
+                               ],
+                               [
+                                       0 => [ 1 => '2dArray' ],
+                               ],
+                               [
+                                       0 => [ 0 => '2dArray', 1 => '2dArray' ],
+                               ],
+                               "Test union of 2d arrays with different keys in the value array",
+                       ],
+                       [
+                               [
+                                       0 => [ 0 => '2dArray' ],
+                               ],
+                               [
+                                       0 => [ 0 => '1dArray' ],
+                               ],
+                               [
+                                       0 => [ 0 => '2dArray' ],
+                               ],
+                               "Test union of 2d arrays with same keys in the value array",
+                       ],
+                       [
+                               [
+                                       0 => [ 0 => [ 0 => '3dArray' ] ],
+                               ],
+                               [
+                                       0 => [ 0 => [ 1 => '2dArray' ] ],
+                               ],
+                               [
+                                       0 => [ 0 => [ 0 => '3dArray' ] ],
+                               ],
+                               "Test union of 3d array with different keys",
+                       ],
+                       [
+                               [
+                                       0 => [ 0 => [ 0 => '3dArray' ] ],
+                               ],
+                               [
+                                       0 => [ 1 => [ 0 => '2dArray' ] ],
+                               ],
+                               [
+                                       0 => [ 0 => [ 0 => '3dArray' ], 1 => [ 0 => '2dArray' ] ],
+                               ],
+                               "Test union of 3d array with different keys in the value array",
+                       ],
+                       [
+                               [
+                                       0 => [ 0 => [ 0 => '3dArray' ] ],
+                               ],
+                               [
+                                       0 => [ 0 => [ 0 => '2dArray' ] ],
+                               ],
+                               [
+                                       0 => [ 0 => [ 0 => '3dArray' ] ],
+                               ],
+                               "Test union of 3d array with same keys in the value array",
+                       ],
+               ];
+       }
+}
diff --git a/tests/phpunit/unit/includes/GlobalFunctions/wfAssembleUrlTest.php b/tests/phpunit/unit/includes/GlobalFunctions/wfAssembleUrlTest.php
new file mode 100644 (file)
index 0000000..f28646e
--- /dev/null
@@ -0,0 +1,112 @@
+<?php
+/**
+ * @group GlobalFunctions
+ * @covers ::wfAssembleUrl
+ */
+class WfAssembleUrlTest extends \MediaWikiUnitTestCase {
+       /**
+        * @dataProvider provideURLParts
+        */
+       public function testWfAssembleUrl( $parts, $output ) {
+               $partsDump = print_r( $parts, true );
+               $this->assertEquals(
+                       $output,
+                       wfAssembleUrl( $parts ),
+                       "Testing $partsDump assembles to $output"
+               );
+       }
+
+       /**
+        * Provider of URL parts for testing wfAssembleUrl()
+        *
+        * @return array
+        */
+       public static function provideURLParts() {
+               $schemes = [
+                       '' => [],
+                       '//' => [
+                               'delimiter' => '//',
+                       ],
+                       'http://' => [
+                               'scheme' => 'http',
+                               'delimiter' => '://',
+                       ],
+               ];
+
+               $hosts = [
+                       '' => [],
+                       'example.com' => [
+                               'host' => 'example.com',
+                       ],
+                       'example.com:123' => [
+                               'host' => 'example.com',
+                               'port' => 123,
+                       ],
+                       'id@example.com' => [
+                               'user' => 'id',
+                               'host' => 'example.com',
+                       ],
+                       'id@example.com:123' => [
+                               'user' => 'id',
+                               'host' => 'example.com',
+                               'port' => 123,
+                       ],
+                       'id:key@example.com' => [
+                               'user' => 'id',
+                               'pass' => 'key',
+                               'host' => 'example.com',
+                       ],
+                       'id:key@example.com:123' => [
+                               'user' => 'id',
+                               'pass' => 'key',
+                               'host' => 'example.com',
+                               'port' => 123,
+                       ],
+               ];
+
+               $cases = [];
+               foreach ( $schemes as $scheme => $schemeParts ) {
+                       foreach ( $hosts as $host => $hostParts ) {
+                               foreach ( [ '', '/path' ] as $path ) {
+                                       foreach ( [ '', 'query' ] as $query ) {
+                                               foreach ( [ '', 'fragment' ] as $fragment ) {
+                                                       $parts = array_merge(
+                                                               $schemeParts,
+                                                               $hostParts
+                                                       );
+                                                       $url = $scheme .
+                                                               $host .
+                                                               $path;
+
+                                                       if ( $path ) {
+                                                               $parts['path'] = $path;
+                                                       }
+                                                       if ( $query ) {
+                                                               $parts['query'] = $query;
+                                                               $url .= '?' . $query;
+                                                       }
+                                                       if ( $fragment ) {
+                                                               $parts['fragment'] = $fragment;
+                                                               $url .= '#' . $fragment;
+                                                       }
+
+                                                       $cases[] = [
+                                                               $parts,
+                                                               $url,
+                                                       ];
+                                               }
+                                       }
+                               }
+                       }
+               }
+
+               $complexURL = 'http://id:key@example.org:321' .
+                       '/over/there?name=ferret&foo=bar#nose';
+               $cases[] = [
+                       wfParseUrl( $complexURL ),
+                       $complexURL,
+               ];
+
+               return $cases;
+       }
+}
diff --git a/tests/phpunit/unit/includes/GlobalFunctions/wfBaseNameTest.php b/tests/phpunit/unit/includes/GlobalFunctions/wfBaseNameTest.php
new file mode 100644 (file)
index 0000000..ac42f3f
--- /dev/null
@@ -0,0 +1,40 @@
+<?php
+/**
+ * @group GlobalFunctions
+ * @covers ::wfBaseName
+ */
+class WfBaseNameTest extends \MediaWikiUnitTestCase {
+       /**
+        * @dataProvider providePaths
+        */
+       public function testBaseName( $fullpath, $basename ) {
+               $this->assertEquals( $basename, wfBaseName( $fullpath ),
+                       "wfBaseName('$fullpath') => '$basename'" );
+       }
+
+       public static function providePaths() {
+               return [
+                       [ '', '' ],
+                       [ '/', '' ],
+                       [ '\\', '' ],
+                       [ '//', '' ],
+                       [ '\\\\', '' ],
+                       [ 'a', 'a' ],
+                       [ 'aaaa', 'aaaa' ],
+                       [ '/a', 'a' ],
+                       [ '\\a', 'a' ],
+                       [ '/aaaa', 'aaaa' ],
+                       [ '\\aaaa', 'aaaa' ],
+                       [ '/aaaa/', 'aaaa' ],
+                       [ '\\aaaa\\', 'aaaa' ],
+                       [ '\\aaaa\\', 'aaaa' ],
+                       [
+                               '/mnt/upload3/wikipedia/en/thumb/8/8b/'
+                                       . 'Zork_Grand_Inquisitor_box_cover.jpg/93px-Zork_Grand_Inquisitor_box_cover.jpg',
+                               '93px-Zork_Grand_Inquisitor_box_cover.jpg'
+                       ],
+                       [ 'C:\\Progra~1\\Wikime~1\\Wikipe~1\\VIEWER.EXE', 'VIEWER.EXE' ],
+                       [ 'Östergötland_coat_of_arms.png', 'Östergötland_coat_of_arms.png' ],
+               ];
+       }
+}
diff --git a/tests/phpunit/unit/includes/GlobalFunctions/wfEscapeShellArgTest.php b/tests/phpunit/unit/includes/GlobalFunctions/wfEscapeShellArgTest.php
new file mode 100644 (file)
index 0000000..7e818df
--- /dev/null
@@ -0,0 +1,43 @@
+<?php
+
+/**
+ * @group GlobalFunctions
+ * @covers ::wfEscapeShellArg
+ */
+class WfEscapeShellArgTest extends \MediaWikiUnitTestCase {
+       public function testSingleInput() {
+               if ( wfIsWindows() ) {
+                       $expected = '"blah"';
+               } else {
+                       $expected = "'blah'";
+               }
+
+               $actual = wfEscapeShellArg( 'blah' );
+
+               $this->assertEquals( $expected, $actual );
+       }
+
+       public function testMultipleArgs() {
+               if ( wfIsWindows() ) {
+                       $expected = '"foo" "bar" "baz"';
+               } else {
+                       $expected = "'foo' 'bar' 'baz'";
+               }
+
+               $actual = wfEscapeShellArg( 'foo', 'bar', 'baz' );
+
+               $this->assertEquals( $expected, $actual );
+       }
+
+       public function testMultipleArgsAsArray() {
+               if ( wfIsWindows() ) {
+                       $expected = '"foo" "bar" "baz"';
+               } else {
+                       $expected = "'foo' 'bar' 'baz'";
+               }
+
+               $actual = wfEscapeShellArg( [ 'foo', 'bar', 'baz' ] );
+
+               $this->assertEquals( $expected, $actual );
+       }
+}
diff --git a/tests/phpunit/unit/includes/GlobalFunctions/wfGetCallerTest.php b/tests/phpunit/unit/includes/GlobalFunctions/wfGetCallerTest.php
new file mode 100644 (file)
index 0000000..c77c351
--- /dev/null
@@ -0,0 +1,46 @@
+<?php
+
+/**
+ * @group GlobalFunctions
+ * @covers ::wfGetCaller
+ */
+class WfGetCallerTest extends \MediaWikiUnitTestCase {
+       public function testZero() {
+               $this->assertEquals( 'WfGetCallerTest->testZero', wfGetCaller( 1 ) );
+       }
+
+       function callerOne() {
+               return wfGetCaller();
+       }
+
+       public function testOne() {
+               $this->assertEquals( 'WfGetCallerTest->testOne', self::callerOne() );
+       }
+
+       static function intermediateFunction( $level = 2, $n = 0 ) {
+               if ( $n > 0 ) {
+                       return self::intermediateFunction( $level, $n - 1 );
+               }
+
+               return wfGetCaller( $level );
+       }
+
+       public function testTwo() {
+               $this->assertEquals( 'WfGetCallerTest->testTwo', self::intermediateFunction() );
+       }
+
+       public function testN() {
+               $this->assertEquals( 'WfGetCallerTest->testN', self::intermediateFunction( 2, 0 ) );
+               $this->assertEquals(
+                       'WfGetCallerTest::intermediateFunction',
+                       self::intermediateFunction( 1, 0 )
+               );
+
+               for ( $i = 0; $i < 10; $i++ ) {
+                       $this->assertEquals(
+                               'WfGetCallerTest::intermediateFunction',
+                               self::intermediateFunction( $i + 1, $i )
+                       );
+               }
+       }
+}
diff --git a/tests/phpunit/unit/includes/GlobalFunctions/wfRemoveDotSegmentsTest.php b/tests/phpunit/unit/includes/GlobalFunctions/wfRemoveDotSegmentsTest.php
new file mode 100644 (file)
index 0000000..085bfed
--- /dev/null
@@ -0,0 +1,93 @@
+<?php
+
+/**
+ * @group GlobalFunctions
+ * @covers ::wfRemoveDotSegments
+ */
+class WfRemoveDotSegmentsTest extends \MediaWikiUnitTestCase {
+       /**
+        * @dataProvider providePaths
+        */
+       public function testWfRemoveDotSegments( $inputPath, $outputPath ) {
+               $this->assertEquals(
+                       $outputPath,
+                       wfRemoveDotSegments( $inputPath ),
+                       "Testing $inputPath expands to $outputPath"
+               );
+       }
+
+       /**
+        * Provider of URL paths for testing wfRemoveDotSegments()
+        *
+        * @return array
+        */
+       public static function providePaths() {
+               return [
+                       [ '/a/b/c/./../../g', '/a/g' ],
+                       [ 'mid/content=5/../6', 'mid/6' ],
+                       [ '/a//../b', '/a/b' ],
+                       [ '/.../a', '/.../a' ],
+                       [ '.../a', '.../a' ],
+                       [ '', '' ],
+                       [ '/', '/' ],
+                       [ '//', '//' ],
+                       [ '.', '' ],
+                       [ '..', '' ],
+                       [ '...', '...' ],
+                       [ '/.', '/' ],
+                       [ '/..', '/' ],
+                       [ './', '' ],
+                       [ '../', '' ],
+                       [ './a', 'a' ],
+                       [ '../a', 'a' ],
+                       [ '../../a', 'a' ],
+                       [ '.././a', 'a' ],
+                       [ './../a', 'a' ],
+                       [ '././a', 'a' ],
+                       [ '../../', '' ],
+                       [ '.././', '' ],
+                       [ './../', '' ],
+                       [ '././', '' ],
+                       [ '../..', '' ],
+                       [ '../.', '' ],
+                       [ './..', '' ],
+                       [ './.', '' ],
+                       [ '/../../a', '/a' ],
+                       [ '/.././a', '/a' ],
+                       [ '/./../a', '/a' ],
+                       [ '/././a', '/a' ],
+                       [ '/../../', '/' ],
+                       [ '/.././', '/' ],
+                       [ '/./../', '/' ],
+                       [ '/././', '/' ],
+                       [ '/../..', '/' ],
+                       [ '/../.', '/' ],
+                       [ '/./..', '/' ],
+                       [ '/./.', '/' ],
+                       [ 'b/../../a', '/a' ],
+                       [ 'b/.././a', '/a' ],
+                       [ 'b/./../a', '/a' ],
+                       [ 'b/././a', 'b/a' ],
+                       [ 'b/../../', '/' ],
+                       [ 'b/.././', '/' ],
+                       [ 'b/./../', '/' ],
+                       [ 'b/././', 'b/' ],
+                       [ 'b/../..', '/' ],
+                       [ 'b/../.', '/' ],
+                       [ 'b/./..', '/' ],
+                       [ 'b/./.', 'b/' ],
+                       [ '/b/../../a', '/a' ],
+                       [ '/b/.././a', '/a' ],
+                       [ '/b/./../a', '/a' ],
+                       [ '/b/././a', '/b/a' ],
+                       [ '/b/../../', '/' ],
+                       [ '/b/.././', '/' ],
+                       [ '/b/./../', '/' ],
+                       [ '/b/././', '/b/' ],
+                       [ '/b/../..', '/' ],
+                       [ '/b/../.', '/' ],
+                       [ '/b/./..', '/' ],
+                       [ '/b/./.', '/b/' ],
+               ];
+       }
+}
diff --git a/tests/phpunit/unit/includes/GlobalFunctions/wfShellExecTest.php b/tests/phpunit/unit/includes/GlobalFunctions/wfShellExecTest.php
new file mode 100644 (file)
index 0000000..09ce624
--- /dev/null
@@ -0,0 +1,20 @@
+<?php
+
+/**
+ * @group GlobalFunctions
+ * @covers ::wfShellExec
+ */
+class WfShellExecTest extends \MediaWikiUnitTestCase {
+       public function testT69870() {
+               $command = wfIsWindows()
+                       // 333 = 331 + CRLF
+                       ? ( 'for /l %i in (1, 1, 1001) do @echo ' . str_repeat( '*', 331 ) )
+                       : 'printf "%-333333s" "*"';
+
+               // Test several times because it involves a race condition that may randomly succeed or fail
+               for ( $i = 0; $i < 10; $i++ ) {
+                       $output = wfShellExec( $command );
+                       $this->assertEquals( 333333, strlen( $output ) );
+               }
+       }
+}
diff --git a/tests/phpunit/unit/includes/GlobalFunctions/wfShorthandToIntegerTest.php b/tests/phpunit/unit/includes/GlobalFunctions/wfShorthandToIntegerTest.php
new file mode 100644 (file)
index 0000000..3bb8b98
--- /dev/null
@@ -0,0 +1,31 @@
+<?php
+
+/**
+ * @group GlobalFunctions
+ * @covers ::wfShorthandToInteger
+ */
+class WfShorthandToIntegerTest extends \MediaWikiUnitTestCase {
+       /**
+        * @dataProvider provideABunchOfShorthands
+        */
+       public function testWfShorthandToInteger( $input, $output, $description ) {
+               $this->assertEquals(
+                       wfShorthandToInteger( $input ),
+                       $output,
+                       $description
+               );
+       }
+
+       public static function provideABunchOfShorthands() {
+               return [
+                       [ '', -1, 'Empty string' ],
+                       [ '     ', -1, 'String of spaces' ],
+                       [ '1G', 1024 * 1024 * 1024, 'One gig uppercased' ],
+                       [ '1g', 1024 * 1024 * 1024, 'One gig lowercased' ],
+                       [ '1M', 1024 * 1024, 'One meg uppercased' ],
+                       [ '1m', 1024 * 1024, 'One meg lowercased' ],
+                       [ '1K', 1024, 'One kb uppercased' ],
+                       [ '1k', 1024, 'One kb lowercased' ],
+               ];
+       }
+}
diff --git a/tests/phpunit/unit/includes/GlobalFunctions/wfStringToBoolTest.php b/tests/phpunit/unit/includes/GlobalFunctions/wfStringToBoolTest.php
new file mode 100644 (file)
index 0000000..bc010d5
--- /dev/null
@@ -0,0 +1,51 @@
+<?php
+
+/**
+ * @group GlobalFunctions
+ * @covers ::wfStringToBool
+ */
+class WfStringToBoolTest extends \MediaWikiUnitTestCase {
+
+       public function getTestCases() {
+               return [
+                       [ 'true', true ],
+                       [ 'on', true ],
+                       [ 'yes', true ],
+                       [ 'TRUE', true ],
+                       [ 'YeS', true ],
+                       [ 'On', true ],
+                       [ '1', true ],
+                       [ '+1', true ],
+                       [ '01', true ],
+                       [ '-001', true ],
+                       [ '  1', true ],
+                       [ '-1  ', true ],
+                       [ '', false ],
+                       [ '0', false ],
+                       [ 'false', false ],
+                       [ 'NO', false ],
+                       [ 'NOT', false ],
+                       [ 'never', false ],
+                       [ '!&', false ],
+                       [ '-0', false ],
+                       [ '+0', false ],
+                       [ 'forget about it', false ],
+                       [ ' on', false ],
+                       [ 'true ', false ],
+               ];
+       }
+
+       /**
+        * @dataProvider getTestCases
+        * @param string $str
+        * @param bool $bool
+        */
+       public function testStr2Bool( $str, $bool ) {
+               if ( $bool ) {
+                       $this->assertTrue( wfStringToBool( $str ) );
+               } else {
+                       $this->assertFalse( wfStringToBool( $str ) );
+               }
+       }
+
+}
diff --git a/tests/phpunit/unit/includes/GlobalFunctions/wfTimestampTest.php b/tests/phpunit/unit/includes/GlobalFunctions/wfTimestampTest.php
new file mode 100644 (file)
index 0000000..78b9172
--- /dev/null
@@ -0,0 +1,194 @@
+<?php
+
+/**
+ * @group GlobalFunctions
+ * @covers ::wfTimestamp
+ */
+class WfTimestampTest extends \MediaWikiUnitTestCase {
+       /**
+        * @dataProvider provideNormalTimestamps
+        */
+       public function testNormalTimestamps( $input, $format, $output, $desc ) {
+               $this->assertEquals( $output, wfTimestamp( $format, $input ), $desc );
+       }
+
+       public static function provideNormalTimestamps() {
+               $t = gmmktime( 12, 34, 56, 1, 15, 2001 );
+
+               return [
+                       // TS_UNIX
+                       [ $t, TS_MW, '20010115123456', 'TS_UNIX to TS_MW' ],
+                       [ -30281104, TS_MW, '19690115123456', 'Negative TS_UNIX to TS_MW' ],
+                       [ $t, TS_UNIX, 979562096, 'TS_UNIX to TS_UNIX' ],
+                       [ $t, TS_DB, '2001-01-15 12:34:56', 'TS_UNIX to TS_DB' ],
+                       [ $t + 0.01, TS_MW, '20010115123456', 'TS_UNIX float to TS_MW' ],
+
+                       [ $t, TS_ISO_8601_BASIC, '20010115T123456Z', 'TS_ISO_8601_BASIC to TS_DB' ],
+
+                       // TS_MW
+                       [ '20010115123456', TS_MW, '20010115123456', 'TS_MW to TS_MW' ],
+                       [ '20010115123456', TS_UNIX, 979562096, 'TS_MW to TS_UNIX' ],
+                       [ '20010115123456', TS_DB, '2001-01-15 12:34:56', 'TS_MW to TS_DB' ],
+                       [ '20010115123456', TS_ISO_8601_BASIC, '20010115T123456Z', 'TS_MW to TS_ISO_8601_BASIC' ],
+
+                       // TS_DB
+                       [ '2001-01-15 12:34:56', TS_MW, '20010115123456', 'TS_DB to TS_MW' ],
+                       [ '2001-01-15 12:34:56', TS_UNIX, 979562096, 'TS_DB to TS_UNIX' ],
+                       [ '2001-01-15 12:34:56', TS_DB, '2001-01-15 12:34:56', 'TS_DB to TS_DB' ],
+                       [
+                               '2001-01-15 12:34:56',
+                               TS_ISO_8601_BASIC,
+                               '20010115T123456Z',
+                               'TS_DB to TS_ISO_8601_BASIC'
+                       ],
+
+                       # rfc2822 section 3.3
+                       [ '20010115123456', TS_RFC2822, 'Mon, 15 Jan 2001 12:34:56 GMT', 'TS_MW to TS_RFC2822' ],
+                       [ 'Mon, 15 Jan 2001 12:34:56 GMT', TS_MW, '20010115123456', 'TS_RFC2822 to TS_MW' ],
+                       [
+                               ' Mon, 15 Jan 2001 12:34:56 GMT',
+                               TS_MW,
+                               '20010115123456',
+                               'TS_RFC2822 with leading space to TS_MW'
+                       ],
+                       [
+                               '15 Jan 2001 12:34:56 GMT',
+                               TS_MW,
+                               '20010115123456',
+                               'TS_RFC2822 without optional day-of-week to TS_MW'
+                       ],
+
+                       # FWS = ([*WSP CRLF] 1*WSP) / obs-FWS ; Folding white space
+                       # obs-FWS = 1*WSP *(CRLF 1*WSP) ; Section 4.2
+                       [ 'Mon, 15         Jan 2001 12:34:56 GMT', TS_MW, '20010115123456', 'TS_RFC2822 to TS_MW' ],
+
+                       # WSP = SP / HTAB ; rfc2234
+                       [
+                               "Mon, 15 Jan\x092001 12:34:56 GMT",
+                               TS_MW,
+                               '20010115123456',
+                               'TS_RFC2822 with HTAB to TS_MW'
+                       ],
+                       [
+                               "Mon, 15 Jan\x09 \x09  2001 12:34:56 GMT",
+                               TS_MW,
+                               '20010115123456',
+                               'TS_RFC2822 with HTAB and SP to TS_MW'
+                       ],
+                       [
+                               'Sun, 6 Nov 94 08:49:37 GMT',
+                               TS_MW,
+                               '19941106084937',
+                               'TS_RFC2822 with obsolete year to TS_MW'
+                       ],
+               ];
+       }
+
+       /**
+        * This test checks wfTimestamp() with values outside.
+        * It needs PHP 64 bits or PHP > 5.1.
+        * See r74778 and T27451
+        * @dataProvider provideOldTimestamps
+        */
+       public function testOldTimestamps( $input, $outputType, $output, $message ) {
+               $timestamp = wfTimestamp( $outputType, $input );
+               if ( substr( $output, 0, 1 ) === '/' ) {
+                       // T66946: Day of the week calculations for very old
+                       // timestamps varies from system to system.
+                       $this->assertRegExp( $output, $timestamp, $message );
+               } else {
+                       $this->assertEquals( $output, $timestamp, $message );
+               }
+       }
+
+       public static function provideOldTimestamps() {
+               return [
+                       [
+                               '19011213204554',
+                               TS_RFC2822,
+                               'Fri, 13 Dec 1901 20:45:54 GMT',
+                               'Earliest time according to PHP documentation'
+                       ],
+                       [ '20380119031407', TS_RFC2822, 'Tue, 19 Jan 2038 03:14:07 GMT', 'Latest 32 bit time' ],
+                       [ '19011213204552', TS_UNIX, '-2147483648', 'Earliest 32 bit unix time' ],
+                       [ '20380119031407', TS_UNIX, '2147483647', 'Latest 32 bit unix time' ],
+                       [ '19011213204552', TS_RFC2822, 'Fri, 13 Dec 1901 20:45:52 GMT', 'Earliest 32 bit time' ],
+                       [
+                               '19011213204551',
+                               TS_RFC2822,
+                               'Fri, 13 Dec 1901 20:45:51 GMT', 'Earliest 32 bit time - 1'
+                       ],
+                       [ '20380119031408', TS_RFC2822, 'Tue, 19 Jan 2038 03:14:08 GMT', 'Latest 32 bit time + 1' ],
+                       [ '19011212000000', TS_MW, '19011212000000', 'Convert to itself r74778#c10645' ],
+                       [ '19011213204551', TS_UNIX, '-2147483649', 'Earliest 32 bit unix time - 1' ],
+                       [ '20380119031408', TS_UNIX, '2147483648', 'Latest 32 bit unix time + 1' ],
+                       [ '-2147483649', TS_MW, '19011213204551', '1901 negative unix time to MediaWiki' ],
+                       [ '-5331871504', TS_MW, '18010115123456', '1801 negative unix time to MediaWiki' ],
+                       [
+                               '0117-08-09 12:34:56',
+                               TS_RFC2822,
+                               '/, 09 Aug 0117 12:34:56 GMT$/',
+                               'Death of Roman Emperor [[Trajan]]'
+                       ],
+
+                       /* @todo FIXME: 00 to 101 years are taken as being in [1970-2069] */
+                       [ '-58979923200', TS_RFC2822, '/, 01 Jan 0101 00:00:00 GMT$/', '1/1/101' ],
+                       [ '-62135596800', TS_RFC2822, 'Mon, 01 Jan 0001 00:00:00 GMT', 'Year 1' ],
+
+                       /* It is not clear if we should generate a year 0 or not
+                        * We are completely off RFC2822 requirement of year being
+                        * 1900 or later.
+                        */
+                       [
+                               '-62142076800',
+                               TS_RFC2822,
+                               'Wed, 18 Oct 0000 00:00:00 GMT',
+                               'ISO 8601:2004 [[year 0]], also called [[1 BC]]'
+                       ],
+               ];
+       }
+
+       /**
+        * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.3.1
+        * @dataProvider provideHttpDates
+        */
+       public function testHttpDate( $input, $output, $desc ) {
+               $this->assertEquals( $output, wfTimestamp( TS_MW, $input ), $desc );
+       }
+
+       public static function provideHttpDates() {
+               return [
+                       [ 'Sun, 06 Nov 1994 08:49:37 GMT', '19941106084937', 'RFC 822 date' ],
+                       [ 'Sunday, 06-Nov-94 08:49:37 GMT', '19941106084937', 'RFC 850 date' ],
+                       [ 'Sun Nov  6 08:49:37 1994', '19941106084937', "ANSI C's asctime() format" ],
+                       // See http://www.squid-cache.org/mail-archive/squid-users/200307/0122.html and r77171
+                       [
+                               'Mon, 22 Nov 2010 14:12:42 GMT; length=52626',
+                               '20101122141242',
+                               'Netscape extension to HTTP/1.0'
+                       ],
+               ];
+       }
+
+       /**
+        * There are a number of assumptions in our codebase where wfTimestamp()
+        * should give the current date but it is not given a 0 there. See r71751 CR
+        */
+       public function testTimestampParameter() {
+               $now = wfTimestamp( TS_UNIX );
+               // We check that wfTimestamp doesn't return false (error) and use a LessThan assert
+               // for the cases where the test is run in a second boundary.
+
+               $zero = wfTimestamp( TS_UNIX, 0 );
+               $this->assertNotEquals( false, $zero );
+               $this->assertLessThan( 5, $zero - $now );
+
+               $empty = wfTimestamp( TS_UNIX, '' );
+               $this->assertNotEquals( false, $empty );
+               $this->assertLessThan( 5, $empty - $now );
+
+               $null = wfTimestamp( TS_UNIX, null );
+               $this->assertNotEquals( false, $null );
+               $this->assertLessThan( 5, $null - $now );
+       }
+}
diff --git a/tests/phpunit/unit/includes/GlobalFunctions/wfUrlencodeTest.php b/tests/phpunit/unit/includes/GlobalFunctions/wfUrlencodeTest.php
new file mode 100644 (file)
index 0000000..a5992d4
--- /dev/null
@@ -0,0 +1,123 @@
+<?php
+
+/**
+ * The function only need a string parameter and might react to IIS7.0
+ *
+ * @group GlobalFunctions
+ * @covers ::wfUrlencode
+ */
+class WfUrlencodeTest extends \MediaWikiUnitTestCase {
+       # ### TESTS ##############################################################
+
+       /**
+        * @dataProvider provideURLS
+        */
+       public function testEncodingUrlWith( $input, $expected ) {
+               $this->verifyEncodingFor( 'Apache', $input, $expected );
+       }
+
+       /**
+        * @dataProvider provideURLS
+        */
+       public function testEncodingUrlWithMicrosoftIis7( $input, $expected ) {
+               $this->verifyEncodingFor( 'Microsoft-IIS/7', $input, $expected );
+       }
+
+       # ### HELPERS #############################################################
+
+       /**
+        * Internal helper that actually run the test.
+        * Called by the public methods testEncodingUrlWith...()
+        */
+       private function verifyEncodingFor( $server, $input, $expectations ) {
+               $expected = $this->extractExpect( $server, $expectations );
+
+               // save up global
+               $old = $_SERVER['SERVER_SOFTWARE'] ?? null;
+               $_SERVER['SERVER_SOFTWARE'] = $server;
+               wfUrlencode( null );
+
+               // do the requested test
+               $this->assertEquals(
+                       $expected,
+                       wfUrlencode( $input ),
+                       "Encoding '$input' for server '$server' should be '$expected'"
+               );
+
+               // restore global
+               if ( $old === null ) {
+                       unset( $_SERVER['SERVER_SOFTWARE'] );
+               } else {
+                       $_SERVER['SERVER_SOFTWARE'] = $old;
+               }
+               wfUrlencode( null );
+       }
+
+       /**
+        * Interprets the provider array. Return expected value depending
+        * the HTTP server name.
+        */
+       private function extractExpect( $server, $expectations ) {
+               if ( is_string( $expectations ) ) {
+                       return $expectations;
+               } elseif ( is_array( $expectations ) ) {
+                       if ( !array_key_exists( $server, $expectations ) ) {
+                               throw new MWException( __METHOD__ . " expectation does not have any "
+                                       . "value for server name $server. Check the provider array.\n" );
+                       } else {
+                               return $expectations[$server];
+                       }
+               } else {
+                       throw new MWException( __METHOD__ . " given invalid expectation for "
+                               . "'$server'. Should be a string or an array( <http server name> => <string> ).\n" );
+               }
+       }
+
+       # ### PROVIDERS ###########################################################
+
+       /**
+        * Format is either:
+        *   [ 'input', 'expected' ];
+        * Or:
+        *   [ 'input',
+        *       [ 'Apache', 'expected' ],
+        *       [ 'Microsoft-IIS/7', 'expected' ],
+        *   ],
+        * If you want to add other HTTP server name, you will have to add a new
+        * testing method much like the testEncodingUrlWith() method above.
+        */
+       public static function provideURLS() {
+               return [
+                       # ## RFC 1738 chars
+                       // + is not safe
+                       [ '+', '%2B' ],
+                       // & and = not safe in queries
+                       [ '&', '%26' ],
+                       [ '=', '%3D' ],
+
+                       [ ':', [
+                               'Apache' => ':',
+                               'Microsoft-IIS/7' => '%3A',
+                       ] ],
+
+                       // remaining chars do not need encoding
+                       [
+                               ';@$-_.!*',
+                               ';@$-_.!*',
+                       ],
+
+                       # ## Other tests
+                       // slash remain unchanged. %2F seems to break things
+                       [ '/', '/' ],
+                       // T105265
+                       [ '~', '~' ],
+
+                       // Other 'funnies' chars
+                       [ '[]', '%5B%5D' ],
+                       [ '<>', '%3C%3E' ],
+
+                       // Apostrophe is encoded
+                       [ '\'', '%27' ],
+               ];
+       }
+}
diff --git a/tests/phpunit/unit/includes/HooksTest.php b/tests/phpunit/unit/includes/HooksTest.php
new file mode 100644 (file)
index 0000000..9380546
--- /dev/null
@@ -0,0 +1,332 @@
+<?php
+
+class HooksTest extends \MediaWikiUnitTestCase {
+
+       function setUp() {
+               global $wgHooks;
+               parent::setUp();
+               Hooks::clear( 'MediaWikiHooksTest001' );
+               unset( $wgHooks['MediaWikiHooksTest001'] );
+       }
+
+       public static function provideHooks() {
+               $i = new NothingClass();
+
+               return [
+                       [
+                               'Object and method',
+                               [ $i, 'someNonStatic' ],
+                               'changed-nonstatic',
+                               'changed-nonstatic'
+                       ],
+                       [ 'Object and no method', [ $i ], 'changed-onevent', 'original' ],
+                       [
+                               'Object and method with data',
+                               [ $i, 'someNonStaticWithData', 'data' ],
+                               'data',
+                               'original'
+                       ],
+                       [ 'Object and static method', [ $i, 'someStatic' ], 'changed-static', 'original' ],
+                       [
+                               'Class::method static call',
+                               [ 'NothingClass::someStatic' ],
+                               'changed-static',
+                               'original'
+                       ],
+                       [
+                               'Class::method static call as array',
+                               [ [ 'NothingClass::someStatic' ] ],
+                               'changed-static',
+                               'original'
+                       ],
+                       [ 'Global function', [ 'NothingFunction' ], 'changed-func', 'original' ],
+                       [ 'Global function with data', [ 'NothingFunctionData', 'data' ], 'data', 'original' ],
+                       [ 'Closure', [ function ( &$foo, $bar ) {
+                               $foo = 'changed-closure';
+
+                               return true;
+                       } ], 'changed-closure', 'original' ],
+                       [ 'Closure with data', [ function ( $data, &$foo, $bar ) {
+                               $foo = $data;
+
+                               return true;
+                       }, 'data' ], 'data', 'original' ]
+               ];
+       }
+
+       /**
+        * @dataProvider provideHooks
+        * @covers Hooks::register
+        * @covers Hooks::run
+        * @covers Hooks::callHook
+        */
+       public function testNewStyleHooks( $msg, $hook, $expectedFoo, $expectedBar ) {
+               $foo = $bar = 'original';
+
+               Hooks::register( 'MediaWikiHooksTest001', $hook );
+               Hooks::run( 'MediaWikiHooksTest001', [ &$foo, &$bar ] );
+
+               $this->assertSame( $expectedFoo, $foo, $msg );
+               $this->assertSame( $expectedBar, $bar, $msg );
+       }
+
+       /**
+        * @covers Hooks::getHandlers
+        */
+       public function testGetHandlers() {
+               global $wgHooks;
+
+               $this->assertSame(
+                       [],
+                       Hooks::getHandlers( 'MediaWikiHooksTest001' ),
+                       'No hooks registered'
+               );
+
+               $a = new NothingClass();
+               $b = new NothingClass();
+
+               $wgHooks['MediaWikiHooksTest001'][] = $a;
+
+               $this->assertSame(
+                       [ $a ],
+                       Hooks::getHandlers( 'MediaWikiHooksTest001' ),
+                       'Hook registered by $wgHooks'
+               );
+
+               Hooks::register( 'MediaWikiHooksTest001', $b );
+               $this->assertSame(
+                       [ $b, $a ],
+                       Hooks::getHandlers( 'MediaWikiHooksTest001' ),
+                       'Hooks::getHandlers() should return hooks registered via wgHooks as well as Hooks::register'
+               );
+
+               Hooks::clear( 'MediaWikiHooksTest001' );
+               unset( $wgHooks['MediaWikiHooksTest001'] );
+
+               Hooks::register( 'MediaWikiHooksTest001', $b );
+               $this->assertSame(
+                       [ $b ],
+                       Hooks::getHandlers( 'MediaWikiHooksTest001' ),
+                       'Hook registered by Hook::register'
+               );
+       }
+
+       /**
+        * @covers Hooks::isRegistered
+        * @covers Hooks::register
+        * @covers Hooks::run
+        * @covers Hooks::callHook
+        */
+       public function testNewStyleHookInteraction() {
+               global $wgHooks;
+
+               $a = new NothingClass();
+               $b = new NothingClass();
+
+               $wgHooks['MediaWikiHooksTest001'][] = $a;
+               $this->assertTrue(
+                       Hooks::isRegistered( 'MediaWikiHooksTest001' ),
+                       'Hook registered via $wgHooks should be noticed by Hooks::isRegistered'
+               );
+
+               Hooks::register( 'MediaWikiHooksTest001', $b );
+               $this->assertEquals(
+                       2,
+                       count( Hooks::getHandlers( 'MediaWikiHooksTest001' ) ),
+                       'Hooks::getHandlers() should return hooks registered via wgHooks as well as Hooks::register'
+               );
+
+               $foo = 'quux';
+               $bar = 'qaax';
+
+               Hooks::run( 'MediaWikiHooksTest001', [ &$foo, &$bar ] );
+               $this->assertEquals(
+                       1,
+                       $a->calls,
+                       'Hooks::run() should run hooks registered via wgHooks as well as Hooks::register'
+               );
+               $this->assertEquals(
+                       1,
+                       $b->calls,
+                       'Hooks::run() should run hooks registered via wgHooks as well as Hooks::register'
+               );
+       }
+
+       /**
+        * @expectedException MWException
+        * @covers Hooks::run
+        * @covers Hooks::callHook
+        */
+       public function testUncallableFunction() {
+               Hooks::register( 'MediaWikiHooksTest001', 'ThisFunctionDoesntExist' );
+               Hooks::run( 'MediaWikiHooksTest001', [] );
+       }
+
+       /**
+        * @covers Hooks::run
+        * @covers Hooks::callHook
+        */
+       public function testFalseReturn() {
+               Hooks::register( 'MediaWikiHooksTest001', function ( &$foo ) {
+                       return false;
+               } );
+               Hooks::register( 'MediaWikiHooksTest001', function ( &$foo ) {
+                       $foo = 'test';
+
+                       return true;
+               } );
+               $foo = 'original';
+               Hooks::run( 'MediaWikiHooksTest001', [ &$foo ] );
+               $this->assertSame( 'original', $foo, 'Hooks abort after a false return.' );
+       }
+
+       /**
+        * @covers Hooks::run
+        */
+       public function testNullReturn() {
+               Hooks::register( 'MediaWikiHooksTest001', function ( &$foo ) {
+                       return;
+               } );
+               Hooks::register( 'MediaWikiHooksTest001', function ( &$foo ) {
+                       $foo = 'test';
+
+                       return true;
+               } );
+               $foo = 'original';
+               Hooks::run( 'MediaWikiHooksTest001', [ &$foo ] );
+               $this->assertSame( 'test', $foo, 'Hooks continue after a null return.' );
+       }
+
+       /**
+        * @covers Hooks::callHook
+        */
+       public function testCallHook_FalseHook() {
+               Hooks::register( 'MediaWikiHooksTest001', false );
+               Hooks::register( 'MediaWikiHooksTest001', function ( &$foo ) {
+                       $foo = 'test';
+
+                       return true;
+               } );
+               $foo = 'original';
+               Hooks::run( 'MediaWikiHooksTest001', [ &$foo ] );
+               $this->assertSame( 'test', $foo, 'Hooks that are falsey are skipped.' );
+       }
+
+       /**
+        * @covers Hooks::callHook
+        * @expectedException MWException
+        */
+       public function testCallHook_UnknownDatatype() {
+               Hooks::register( 'MediaWikiHooksTest001', 12345 );
+               Hooks::run( 'MediaWikiHooksTest001' );
+       }
+
+       /**
+        * @covers Hooks::callHook
+        * @expectedException PHPUnit_Framework_Error_Deprecated
+        */
+       public function testCallHook_Deprecated() {
+               Hooks::register( 'MediaWikiHooksTest001', 'NothingClass::someStatic' );
+               Hooks::run( 'MediaWikiHooksTest001', [], '1.31' );
+       }
+
+       /**
+        * @covers Hooks::runWithoutAbort
+        * @covers Hooks::callHook
+        */
+       public function testRunWithoutAbort() {
+               $list = [];
+               Hooks::register( 'MediaWikiHooksTest001', function ( &$list ) {
+                       $list[] = 1;
+                       return true; // Explicit true
+               } );
+               Hooks::register( 'MediaWikiHooksTest001', function ( &$list ) {
+                       $list[] = 2;
+                       return; // Implicit null
+               } );
+               Hooks::register( 'MediaWikiHooksTest001', function ( &$list ) {
+                       $list[] = 3;
+                       // No return
+               } );
+
+               Hooks::runWithoutAbort( 'MediaWikiHooksTest001', [ &$list ] );
+               $this->assertSame( [ 1, 2, 3 ], $list, 'All hooks ran.' );
+       }
+
+       /**
+        * @covers Hooks::runWithoutAbort
+        * @covers Hooks::callHook
+        */
+       public function testRunWithoutAbortWarning() {
+               Hooks::register( 'MediaWikiHooksTest001', function ( &$foo ) {
+                       return false;
+               } );
+               Hooks::register( 'MediaWikiHooksTest001', function ( &$foo ) {
+                       $foo = 'test';
+                       return true;
+               } );
+               $foo = 'original';
+
+               $this->setExpectedException(
+                       UnexpectedValueException::class,
+                       'Invalid return from hook-MediaWikiHooksTest001-closure for ' .
+                               'unabortable MediaWikiHooksTest001'
+               );
+               Hooks::runWithoutAbort( 'MediaWikiHooksTest001', [ &$foo ] );
+       }
+
+       /**
+        * @expectedException FatalError
+        * @covers Hooks::run
+        */
+       public function testFatalError() {
+               Hooks::register( 'MediaWikiHooksTest001', function () {
+                       return 'test';
+               } );
+               Hooks::run( 'MediaWikiHooksTest001', [] );
+       }
+}
+
+function NothingFunction( &$foo, &$bar ) {
+       $foo = 'changed-func';
+
+       return true;
+}
+
+function NothingFunctionData( $data, &$foo, &$bar ) {
+       $foo = $data;
+
+       return true;
+}
+
+class NothingClass {
+       public $calls = 0;
+
+       public static function someStatic( &$foo, &$bar ) {
+               $foo = 'changed-static';
+
+               return true;
+       }
+
+       public function someNonStatic( &$foo, &$bar ) {
+               $this->calls++;
+               $foo = 'changed-nonstatic';
+               $bar = 'changed-nonstatic';
+
+               return true;
+       }
+
+       public function onMediaWikiHooksTest001( &$foo, &$bar ) {
+               $this->calls++;
+               $foo = 'changed-onevent';
+
+               return true;
+       }
+
+       public function someNonStaticWithData( $data, &$foo, &$bar ) {
+               $this->calls++;
+               $foo = $data;
+
+               return true;
+       }
+}
diff --git a/tests/phpunit/unit/includes/LicensesTest.php b/tests/phpunit/unit/includes/LicensesTest.php
new file mode 100644 (file)
index 0000000..e5a6bae
--- /dev/null
@@ -0,0 +1,25 @@
+<?php
+
+/**
+ * @covers Licenses
+ */
+class LicensesTest extends \MediaWikiUnitTestCase {
+
+       public function testLicenses() {
+               $str = "
+* Free licenses:
+** GFDL|Debian disagrees
+";
+
+               $lc = new Licenses( [
+                       'fieldname' => 'FooField',
+                       'type' => 'select',
+                       'section' => 'description',
+                       'id' => 'wpLicense',
+                       'label' => 'A label text', # Note can't test label-message because $wgOut is not defined
+                       'name' => 'AnotherName',
+                       'licenses' => $str,
+               ] );
+               $this->assertThat( $lc, $this->isInstanceOf( Licenses::class ) );
+       }
+}
diff --git a/tests/phpunit/unit/includes/ListToggleTest.php b/tests/phpunit/unit/includes/ListToggleTest.php
new file mode 100644 (file)
index 0000000..0ff65bb
--- /dev/null
@@ -0,0 +1,49 @@
+<?php
+
+/**
+ * @covers ListToggle
+ */
+class ListToggleTest extends \MediaWikiUnitTestCase {
+
+       /**
+        * @covers ListToggle::__construct
+        */
+       public function testConstruct() {
+               $output = $this->getMockBuilder( OutputPage::class )
+                       ->setMethods( null )
+                       ->disableOriginalConstructor()
+                       ->getMock();
+
+               $listToggle = new ListToggle( $output );
+
+               $this->assertInstanceOf( ListToggle::class, $listToggle );
+               $this->assertContains( 'mediawiki.checkboxtoggle', $output->getModules() );
+               $this->assertContains( 'mediawiki.checkboxtoggle.styles', $output->getModuleStyles() );
+       }
+
+       /**
+        * @covers ListToggle::getHTML
+        */
+       public function testGetHTML() {
+               $output = $this->createMock( OutputPage::class );
+               $output->expects( $this->any() )
+                       ->method( 'msg' )
+                       ->will( $this->returnCallback( function ( $key ) {
+                               return wfMessage( $key )->inLanguage( 'qqx' );
+                       } ) );
+               $output->expects( $this->once() )
+                       ->method( 'getLanguage' )
+                       ->will( $this->returnValue( Language::factory( 'qqx' ) ) );
+
+               $listToggle = new ListToggle( $output );
+
+               $html = $listToggle->getHTML();
+               $this->assertEquals( '<div class="mw-checkbox-toggle-controls">' .
+                       '(checkbox-select: <a class="mw-checkbox-all" role="button"' .
+                       ' tabindex="0">(checkbox-all)</a>(comma-separator)' .
+                       '<a class="mw-checkbox-none" role="button" tabindex="0">' .
+                       '(checkbox-none)</a>(comma-separator)<a class="mw-checkbox-invert" ' .
+                       'role="button" tabindex="0">(checkbox-invert)</a>)</div>',
+                       $html );
+       }
+}
diff --git a/tests/phpunit/unit/includes/MagicWordFactoryTest.php b/tests/phpunit/unit/includes/MagicWordFactoryTest.php
new file mode 100644 (file)
index 0000000..14a4727
--- /dev/null
@@ -0,0 +1,84 @@
+<?php
+
+/**
+ * @covers \MagicWordFactory
+ *
+ * @author Derick N. Alangi
+ */
+class MagicWordFactoryTest extends \MediaWikiUnitTestCase {
+       private function makeMagicWordFactory( Language $contLang = null ) {
+               return new MagicWordFactory( $contLang ?: Language::factory( 'en' ) );
+       }
+
+       public function testGetContentLanguage() {
+               $contLang = Language::factory( 'en' );
+
+               $magicWordFactory = $this->makeMagicWordFactory( $contLang );
+               $magicWordContLang = $magicWordFactory->getContentLanguage();
+
+               $this->assertSame( $contLang, $magicWordContLang );
+       }
+
+       public function testGetMagicWord() {
+               $magicWordIdValid = 'pageid';
+               $magicWordFactory = $this->makeMagicWordFactory();
+               $mwActual = $magicWordFactory->get( $magicWordIdValid );
+               $contLang = $magicWordFactory->getContentLanguage();
+               $expected = new MagicWord( $magicWordIdValid, [ 'PAGEID' ], false, $contLang );
+
+               $this->assertEquals( $expected, $mwActual );
+       }
+
+       public function testGetInvalidMagicWord() {
+               $magicWordFactory = $this->makeMagicWordFactory();
+
+               $this->setExpectedException( MWException::class );
+               \Wikimedia\suppressWarnings();
+               try {
+                       $magicWordFactory->get( 'invalid magic word' );
+               } finally {
+                       \Wikimedia\restoreWarnings();
+               }
+       }
+
+       public function testGetVariableIDs() {
+               $magicWordFactory = $this->makeMagicWordFactory();
+               $varIds = $magicWordFactory->getVariableIDs();
+
+               $this->assertInternalType( 'array', $varIds );
+               $this->assertNotEmpty( $varIds );
+               $this->assertContainsOnly( 'string', $varIds );
+       }
+
+       public function testGetSubstIDs() {
+               $magicWordFactory = $this->makeMagicWordFactory();
+               $substIds = $magicWordFactory->getSubstIDs();
+
+               $this->assertInternalType( 'array', $substIds );
+               $this->assertNotEmpty( $substIds );
+               $this->assertContainsOnly( 'string', $substIds );
+       }
+
+       /**
+        * Test both valid and invalid caching hints paths
+        */
+       public function testGetCacheTTL() {
+               $magicWordFactory = $this->makeMagicWordFactory();
+               $actual = $magicWordFactory->getCacheTTL( 'localday' );
+
+               $this->assertSame( 3600, $actual );
+
+               $actual = $magicWordFactory->getCacheTTL( 'currentmonth' );
+               $this->assertSame( 86400, $actual );
+
+               $actual = $magicWordFactory->getCacheTTL( 'invalid' );
+               $this->assertSame( -1, $actual );
+       }
+
+       public function testGetDoubleUnderscoreArray() {
+               $magicWordFactory = $this->makeMagicWordFactory();
+               $actual = $magicWordFactory->getDoubleUnderscoreArray();
+
+               $this->assertInstanceOf( MagicWordArray::class, $actual );
+       }
+}
diff --git a/tests/phpunit/unit/includes/MediaWikiServicesTest.php b/tests/phpunit/unit/includes/MediaWikiServicesTest.php
new file mode 100644 (file)
index 0000000..c89c820
--- /dev/null
@@ -0,0 +1,372 @@
+<?php
+
+use MediaWiki\MediaWikiServices;
+use Wikimedia\Services\DestructibleService;
+use Wikimedia\Services\SalvageableService;
+use Wikimedia\Services\ServiceDisabledException;
+
+/**
+ * @covers MediaWiki\MediaWikiServices
+ *
+ * @group MediaWiki
+ */
+class MediaWikiServicesTest extends \MediaWikiUnitTestCase {
+       private $deprecatedServices = [];
+
+       /**
+        * @return Config
+        */
+       private function newTestConfig() {
+               $globalConfig = new GlobalVarConfig();
+
+               $testConfig = new HashConfig();
+               $testConfig->set( 'ServiceWiringFiles', $globalConfig->get( 'ServiceWiringFiles' ) );
+               $testConfig->set( 'ConfigRegistry', $globalConfig->get( 'ConfigRegistry' ) );
+
+               return $testConfig;
+       }
+
+       /**
+        * @return MediaWikiServices
+        */
+       private function newMediaWikiServices( Config $config = null ) {
+               if ( $config === null ) {
+                       $config = $this->newTestConfig();
+               }
+
+               $instance = new MediaWikiServices( $config );
+
+               // Load the default wiring from the specified files.
+               $wiringFiles = $config->get( 'ServiceWiringFiles' );
+               $instance->loadWiringFiles( $wiringFiles );
+
+               return $instance;
+       }
+
+       public function testGetInstance() {
+               $services = MediaWikiServices::getInstance();
+               $this->assertInstanceOf( MediaWikiServices::class, $services );
+       }
+
+       public function testForceGlobalInstance() {
+               $newServices = $this->newMediaWikiServices();
+               $oldServices = MediaWikiServices::forceGlobalInstance( $newServices );
+
+               $this->assertInstanceOf( MediaWikiServices::class, $oldServices );
+               $this->assertNotSame( $oldServices, $newServices );
+
+               $theServices = MediaWikiServices::getInstance();
+               $this->assertSame( $theServices, $newServices );
+
+               MediaWikiServices::forceGlobalInstance( $oldServices );
+
+               $theServices = MediaWikiServices::getInstance();
+               $this->assertSame( $theServices, $oldServices );
+       }
+
+       public function testResetGlobalInstance() {
+               $newServices = $this->newMediaWikiServices();
+               $oldServices = MediaWikiServices::forceGlobalInstance( $newServices );
+
+               $service1 = $this->createMock( SalvageableService::class );
+               $service1->expects( $this->never() )
+                       ->method( 'salvage' );
+
+               $newServices->defineService(
+                       'Test',
+                       function () use ( $service1 ) {
+                               return $service1;
+                       }
+               );
+
+               // force instantiation
+               $newServices->getService( 'Test' );
+
+               MediaWikiServices::resetGlobalInstance( $this->newTestConfig() );
+               $theServices = MediaWikiServices::getInstance();
+
+               $this->assertSame(
+                       $service1,
+                       $theServices->getService( 'Test' ),
+                       'service definition should survive reset'
+               );
+
+               $this->assertNotSame( $theServices, $newServices );
+               $this->assertNotSame( $theServices, $oldServices );
+
+               MediaWikiServices::forceGlobalInstance( $oldServices );
+       }
+
+       public function testResetGlobalInstance_quick() {
+               $newServices = $this->newMediaWikiServices();
+               $oldServices = MediaWikiServices::forceGlobalInstance( $newServices );
+
+               $service1 = $this->createMock( SalvageableService::class );
+               $service1->expects( $this->never() )
+                       ->method( 'salvage' );
+
+               $service2 = $this->createMock( SalvageableService::class );
+               $service2->expects( $this->once() )
+                       ->method( 'salvage' )
+                       ->with( $service1 );
+
+               // sequence of values the instantiator will return
+               $instantiatorReturnValues = [
+                       $service1,
+                       $service2,
+               ];
+
+               $newServices->defineService(
+                       'Test',
+                       function () use ( &$instantiatorReturnValues ) {
+                               return array_shift( $instantiatorReturnValues );
+                       }
+               );
+
+               // force instantiation
+               $newServices->getService( 'Test' );
+
+               MediaWikiServices::resetGlobalInstance( $this->newTestConfig(), 'quick' );
+               $theServices = MediaWikiServices::getInstance();
+
+               $this->assertSame( $service2, $theServices->getService( 'Test' ) );
+
+               $this->assertNotSame( $theServices, $newServices );
+               $this->assertNotSame( $theServices, $oldServices );
+
+               MediaWikiServices::forceGlobalInstance( $oldServices );
+       }
+
+       public function testDisableStorageBackend() {
+               $newServices = $this->newMediaWikiServices();
+               $oldServices = MediaWikiServices::forceGlobalInstance( $newServices );
+
+               $lbFactory = $this->getMockBuilder( \Wikimedia\Rdbms\LBFactorySimple::class )
+                       ->disableOriginalConstructor()
+                       ->getMock();
+
+               $newServices->redefineService(
+                       'DBLoadBalancerFactory',
+                       function () use ( $lbFactory ) {
+                               return $lbFactory;
+                       }
+               );
+
+               // force the service to become active, so we can check that it does get destroyed
+               $newServices->getService( 'DBLoadBalancerFactory' );
+
+               MediaWikiServices::disableStorageBackend(); // should destroy DBLoadBalancerFactory
+
+               try {
+                       MediaWikiServices::getInstance()->getService( 'DBLoadBalancerFactory' );
+                       $this->fail( 'DBLoadBalancerFactory should have been disabled' );
+               }
+               catch ( ServiceDisabledException $ex ) {
+                       // ok, as expected
+               } catch ( Throwable $ex ) {
+                       $this->fail( 'ServiceDisabledException expected, caught ' . get_class( $ex ) );
+               }
+
+               MediaWikiServices::forceGlobalInstance( $oldServices );
+               $newServices->destroy();
+
+               // No exception was thrown, avoid being risky
+               $this->assertTrue( true );
+       }
+
+       public function testResetChildProcessServices() {
+               $newServices = $this->newMediaWikiServices();
+               $oldServices = MediaWikiServices::forceGlobalInstance( $newServices );
+
+               $service1 = $this->createMock( DestructibleService::class );
+               $service1->expects( $this->once() )
+                       ->method( 'destroy' );
+
+               $service2 = $this->createMock( DestructibleService::class );
+               $service2->expects( $this->never() )
+                       ->method( 'destroy' );
+
+               // sequence of values the instantiator will return
+               $instantiatorReturnValues = [
+                       $service1,
+                       $service2,
+               ];
+
+               $newServices->defineService(
+                       'Test',
+                       function () use ( &$instantiatorReturnValues ) {
+                               return array_shift( $instantiatorReturnValues );
+                       }
+               );
+
+               // force the service to become active, so we can check that it does get destroyed
+               $oldTestService = $newServices->getService( 'Test' );
+
+               MediaWikiServices::resetChildProcessServices();
+               $finalServices = MediaWikiServices::getInstance();
+
+               $newTestService = $finalServices->getService( 'Test' );
+               $this->assertNotSame( $oldTestService, $newTestService );
+
+               MediaWikiServices::forceGlobalInstance( $oldServices );
+       }
+
+       public function testResetServiceForTesting() {
+               $services = $this->newMediaWikiServices();
+               $serviceCounter = 0;
+
+               $services->defineService(
+                       'Test',
+                       function () use ( &$serviceCounter ) {
+                               $serviceCounter++;
+                               $service = $this->createMock( Wikimedia\Services\DestructibleService::class );
+                               $service->expects( $this->once() )->method( 'destroy' );
+                               return $service;
+                       }
+               );
+
+               // This should do nothing. In particular, it should not create a service instance.
+               $services->resetServiceForTesting( 'Test' );
+               $this->assertEquals( 0, $serviceCounter, 'No service instance should be created yet.' );
+
+               $oldInstance = $services->getService( 'Test' );
+               $this->assertEquals( 1, $serviceCounter, 'A service instance should exit now.' );
+
+               // The old instance should be detached, and destroy() called.
+               $services->resetServiceForTesting( 'Test' );
+               $newInstance = $services->getService( 'Test' );
+
+               $this->assertNotSame( $oldInstance, $newInstance );
+
+               // Satisfy the expectation that destroy() is called also for the second service instance.
+               $newInstance->destroy();
+       }
+
+       public function testResetServiceForTesting_noDestroy() {
+               $services = $this->newMediaWikiServices();
+
+               $services->defineService(
+                       'Test',
+                       function () {
+                               $service = $this->createMock( Wikimedia\Services\DestructibleService::class );
+                               $service->expects( $this->never() )->method( 'destroy' );
+                               return $service;
+                       }
+               );
+
+               $oldInstance = $services->getService( 'Test' );
+
+               // The old instance should be detached, but destroy() not called.
+               $services->resetServiceForTesting( 'Test', false );
+               $newInstance = $services->getService( 'Test' );
+
+               $this->assertNotSame( $oldInstance, $newInstance );
+       }
+
+       public function provideGetters() {
+               $getServiceCases = $this->provideGetService();
+               $getterCases = [];
+
+               // All getters should be named just like the service, with "get" added.
+               foreach ( $getServiceCases as $name => $case ) {
+                       if ( $name[0] === '_' ) {
+                               // Internal service, no getter
+                               continue;
+                       }
+                       list( $service, $class ) = $case;
+                       $getterCases[$name] = [
+                               'get' . $service,
+                               $class,
+                               in_array( $service, $this->deprecatedServices )
+                       ];
+               }
+
+               return $getterCases;
+       }
+
+       /**
+        * @dataProvider provideGetters
+        */
+       public function testGetters( $getter, $type, $isDeprecated = false ) {
+               if ( $isDeprecated ) {
+                       $this->hideDeprecated( MediaWikiServices::class . "::$getter" );
+               }
+
+               // Check all services via an instance using the global configuration, not a dummy instance!
+               $services = $this->newMediaWikiServices( new GlobalVarConfig() );
+               $service = $services->$getter();
+               $this->assertInstanceOf( $type, $service );
+       }
+
+       public function provideGetService() {
+               global $IP;
+               $serviceList = require "$IP/includes/ServiceWiring.php";
+               $ret = [];
+               foreach ( $serviceList as $name => $callback ) {
+                       $fun = new ReflectionFunction( $callback );
+                       if ( !$fun->hasReturnType() ) {
+                               throw new MWException( 'All service callbacks must have a return type defined, ' .
+                                       "none found for $name" );
+                       }
+                       $ret[$name] = [ $name, $fun->getReturnType()->__toString() ];
+               }
+               return $ret;
+       }
+
+       /**
+        * @dataProvider provideGetService
+        */
+       public function testGetService( $name, $type ) {
+               // Check all services via an instance using the global configuration, not a dummy instance!
+               $services = $this->newMediaWikiServices( new GlobalVarConfig() );
+
+               $service = $services->getService( $name );
+               $this->assertInstanceOf( $type, $service );
+       }
+
+       public function testDefaultServiceInstantiation() {
+               // Check all services via an instance using the global configuration, not a dummy instance!
+               // Note that we instantiate all services here, including any that
+               // were registered by extensions.
+               $services = $this->newMediaWikiServices( new GlobalVarConfig() );
+               $names = $services->getServiceNames();
+
+               foreach ( $names as $name ) {
+                       $this->assertTrue( $services->hasService( $name ) );
+                       $service = $services->getService( $name );
+                       $this->assertInternalType( 'object', $service );
+               }
+       }
+
+       public function testDefaultServiceWiringServicesHaveTests() {
+               global $IP;
+               $testedServices = array_keys( $this->provideGetService() );
+               $allServices = array_keys( require "$IP/includes/ServiceWiring.php" );
+               $this->assertEquals(
+                       [],
+                       array_diff( $allServices, $testedServices ),
+                       'The following services have not been added to MediaWikiServicesTest::provideGetService'
+               );
+       }
+
+       public function testGettersAreSorted() {
+               $methods = ( new ReflectionClass( MediaWikiServices::class ) )
+                       ->getMethods( ReflectionMethod::IS_STATIC | ReflectionMethod::IS_PUBLIC );
+
+               $names = array_map( function ( $method ) {
+                       return $method->getName();
+               }, $methods );
+               $serviceNames = array_map( function ( $name ) {
+                       return "get$name";
+               }, array_keys( $this->provideGetService() ) );
+               $names = array_values( array_filter( $names, function ( $name ) use ( $serviceNames ) {
+                       return in_array( $name, $serviceNames );
+               } ) );
+
+               $sortedNames = $names;
+               natcasesort( $sortedNames );
+
+               $this->assertSame( $sortedNames, $names,
+                       'Please keep service getters sorted alphabetically' );
+       }
+}
diff --git a/tests/phpunit/unit/includes/MediaWikiVersionFetcherTest.php b/tests/phpunit/unit/includes/MediaWikiVersionFetcherTest.php
new file mode 100644 (file)
index 0000000..dfdbfa7
--- /dev/null
@@ -0,0 +1,21 @@
+<?php
+
+/**
+ * Note: this is not a unit test, as it touches the file system and reads an actual file.
+ * If unit tests are added for MediaWikiVersionFetcher, this should be done in a distinct test case.
+ *
+ * @covers MediaWikiVersionFetcher
+ *
+ * @group ComposerHooks
+ *
+ * @author Jeroen De Dauw < jeroendedauw@gmail.com >
+ */
+class MediaWikiVersionFetcherTest extends \MediaWikiUnitTestCase {
+
+       public function testReturnsResult() {
+               global $wgVersion;
+               $versionFetcher = new MediaWikiVersionFetcher();
+               $this->assertSame( $wgVersion, $versionFetcher->fetchVersion() );
+       }
+
+}
diff --git a/tests/phpunit/unit/includes/PathRouterTest.php b/tests/phpunit/unit/includes/PathRouterTest.php
new file mode 100644 (file)
index 0000000..0cb6c81
--- /dev/null
@@ -0,0 +1,325 @@
+<?php
+
+/**
+ * Tests for the PathRouter parsing.
+ *
+ * @covers PathRouter
+ */
+class PathRouterTest extends \MediaWikiUnitTestCase {
+
+       /**
+        * @var PathRouter
+        */
+       protected $basicRouter;
+
+       protected function setUp() {
+               parent::setUp();
+               $router = new PathRouter;
+               $router->add( "/wiki/$1" );
+               $this->basicRouter = $router;
+       }
+
+       public static function provideParse() {
+               $tests = [
+                       // Basic path parsing
+                       'Basic path parsing' => [
+                               "/wiki/$1",
+                               "/wiki/Foo",
+                               [ 'title' => "Foo" ]
+                       ],
+                       //
+                       'Loose path auto-$1: /$1' => [
+                               "/",
+                               "/Foo",
+                               [ 'title' => "Foo" ]
+                       ],
+                       'Loose path auto-$1: /wiki' => [
+                               "/wiki",
+                               "/wiki/Foo",
+                               [ 'title' => "Foo" ]
+                       ],
+                       'Loose path auto-$1: /wiki/' => [
+                               "/wiki/",
+                               "/wiki/Foo",
+                               [ 'title' => "Foo" ]
+                       ],
+                       // Ensure that path is based on specificity, not order
+                       'Order, /$1 added first' => [
+                               [ "/$1", "/a/$1", "/b/$1" ],
+                               "/a/Foo",
+                               [ 'title' => "Foo" ]
+                       ],
+                       'Order, /$1 added last' => [
+                               [ "/b/$1", "/a/$1", "/$1" ],
+                               "/a/Foo",
+                               [ 'title' => "Foo" ]
+                       ],
+                       // Handling of key based arrays with a url parameter
+                       'Key based array' => [
+                               [ [
+                                       'path' => [ 'edit' => "/edit/$1" ],
+                                       'params' => [ 'action' => '$key' ],
+                               ] ],
+                               "/edit/Foo",
+                               [ 'title' => "Foo", 'action' => 'edit' ]
+                       ],
+                       // Additional parameter
+                       'Basic $2' => [
+                               [ [
+                                       'path' => '/$2/$1',
+                                       'params' => [ 'test' => '$2' ]
+                               ] ],
+                               "/asdf/Foo",
+                               [ 'title' => "Foo", 'test' => 'asdf' ]
+                       ],
+               ];
+               // Shared patterns for restricted value parameter tests
+               $restrictedPatterns = [
+                       [
+                               'path' => '/$2/$1',
+                               'params' => [ 'test' => '$2' ],
+                               'options' => [ '$2' => [ 'a', 'b' ] ]
+                       ],
+                       [
+                               'path' => '/$2/$1',
+                               'params' => [ 'test2' => '$2' ],
+                               'options' => [ '$2' => 'c' ]
+                       ],
+                       '/$1'
+               ];
+               $tests += [
+                       // Restricted value parameter tests
+                       'Restricted 1' => [
+                               $restrictedPatterns,
+                               "/asdf/Foo",
+                               [ 'title' => "asdf/Foo" ]
+                       ],
+                       'Restricted 2' => [
+                               $restrictedPatterns,
+                               "/a/Foo",
+                               [ 'title' => "Foo", 'test' => 'a' ]
+                       ],
+                       'Restricted 3' => [
+                               $restrictedPatterns,
+                               "/c/Foo",
+                               [ 'title' => "Foo", 'test2' => 'c' ]
+                       ],
+
+                       // Callback test
+                       'Callback' => [
+                               [ [
+                                       'path' => "/$1",
+                                       'params' => [ 'a' => 'b', 'data:foo' => 'bar' ],
+                                       'options' => [ 'callback' => [ __CLASS__, 'callbackForTest' ] ]
+                               ] ],
+                               '/Foo',
+                               [
+                                       'title' => "Foo",
+                                       'x' => 'Foo',
+                                       'a' => 'b',
+                                       'foo' => 'bar'
+                               ]
+                       ],
+
+                       // Test to ensure that matches are not made if a parameter expects nonexistent input
+                       'Fail' => [
+                               [ [
+                                       'path' => "/wiki/$1",
+                                       'params' => [ 'title' => "$1$2" ],
+                               ] ],
+                               "/wiki/A",
+                               []
+                       ],
+
+                       // Make sure the router handles titles like Special:Recentchanges correctly
+                       'Special title' => [
+                               "/wiki/$1",
+                               "/wiki/Special:Recentchanges",
+                               [ 'title' => "Special:Recentchanges" ]
+                       ],
+
+                       // Make sure the router decodes urlencoding properly
+                       'URL encoding' => [
+                               "/wiki/$1",
+                               "/wiki/Title_With%20Space",
+                               [ 'title' => "Title_With Space" ]
+                       ],
+
+                       // Double slash and dot expansion
+                       'Double slash in prefix' => [
+                               '/wiki/$1',
+                               '//wiki/Foo',
+                               [ 'title' => 'Foo' ]
+                       ],
+                       'Double slash at start of $1' => [
+                               '/wiki/$1',
+                               '/wiki//Foo',
+                               [ 'title' => '/Foo' ]
+                       ],
+                       'Double slash in middle of $1' => [
+                               '/wiki/$1',
+                               '/wiki/.hack//SIGN',
+                               [ 'title' => '.hack//SIGN' ]
+                       ],
+                       'Dots removed 1' => [
+                               '/wiki/$1',
+                               '/x/../wiki/Foo',
+                               [ 'title' => 'Foo' ]
+                       ],
+                       'Dots removed 2' => [
+                               '/wiki/$1',
+                               '/./wiki/Foo',
+                               [ 'title' => 'Foo' ]
+                       ],
+                       'Dots retained 1' => [
+                               '/wiki/$1',
+                               '/wiki/../wiki/Foo',
+                               [ 'title' => '../wiki/Foo' ]
+                       ],
+                       'Dots retained 2' => [
+                               '/wiki/$1',
+                               '/wiki/./Foo',
+                               [ 'title' => './Foo' ]
+                       ],
+                       'Triple slash' => [
+                               '/wiki/$1',
+                               '///wiki/Foo',
+                               [ 'title' => 'Foo' ]
+                       ],
+                       // '..' only traverses one slash, see e.g. RFC 3986
+                       'Dots traversing double slash 1' => [
+                               '/wiki/$1',
+                               '/a//b/../../wiki/Foo',
+                               []
+                       ],
+                       'Dots traversing double slash 2' => [
+                               '/wiki/$1',
+                               '/a//b/../../../wiki/Foo',
+                               [ 'title' => 'Foo' ]
+                       ],
+               ];
+
+               // Make sure the router doesn't break on special characters like $ used in regexp replacements
+               foreach ( [ "$", "$1", "\\", "\\$1" ] as $char ) {
+                       $tests["Regexp character $char"] = [
+                               "/wiki/$1",
+                               "/wiki/$char",
+                               [ 'title' => "$char" ]
+                       ];
+               }
+
+               $tests += [
+                       // Make sure the router handles characters like +&() properly
+                       "Special characters" => [
+                               "/wiki/$1",
+                               "/wiki/Plus+And&Dollar\\Stuff();[]{}*",
+                               [ 'title' => "Plus+And&Dollar\\Stuff();[]{}*" ],
+                       ],
+
+                       // Make sure the router handles unicode characters correctly
+                       "Unicode 1" => [
+                               "/wiki/$1",
+                               "/wiki/Spécial:Modifications_récentes" ,
+                               [ 'title' => "Spécial:Modifications_récentes" ],
+                       ],
+
+                       "Unicode 2" => [
+                               "/wiki/$1",
+                               "/wiki/Sp%C3%A9cial:Modifications_r%C3%A9centes",
+                               [ 'title' => "Spécial:Modifications_récentes" ],
+                       ]
+               ];
+
+               // Ensure the router doesn't choke on long paths.
+               $lorem = "Lorem_ipsum_dolor_sit_amet,_consectetur_adipisicing_elit,_sed_do_eiusmod_" .
+                       "tempor_incididunt_ut_labore_et_dolore_magna_aliqua._Ut_enim_ad_minim_veniam,_quis_" .
+                        "nostrud_exercitation_ullamco_laboris_nisi_ut_aliquip_ex_ea_commodo_consequat._" .
+                        "Duis_aute_irure_dolor_in_reprehenderit_in_voluptate_velit_esse_cillum_dolore_" .
+                        "eu_fugiat_nulla_pariatur._Excepteur_sint_occaecat_cupidatat_non_proident,_sunt_" .
+                        "in_culpa_qui_officia_deserunt_mollit_anim_id_est_laborum.";
+
+               $tests += [
+                       "Long path" => [
+                               "/wiki/$1",
+                               "/wiki/$lorem",
+                               [ 'title' => $lorem ]
+                       ],
+
+                       // Ensure that the php passed site of parameter values are not urldecoded
+                       "Pattern urlencoding" => [
+                               [ [ 'path' => "/wiki/$1", 'params' => [ 'title' => '%20:$1' ] ] ],
+                               "/wiki/Foo",
+                               [ 'title' => '%20:Foo' ]
+                       ],
+
+                       // Ensure that raw parameter values do not have any variable replacements or urldecoding
+                       "Raw param value" => [
+                               [ [ 'path' => "/wiki/$1", 'params' => [ 'title' => [ 'value' => 'bar%20$1' ] ] ] ],
+                               "/wiki/Foo",
+                               [ 'title' => 'bar%20$1' ]
+                       ]
+               ];
+
+               return $tests;
+       }
+
+       /**
+        * Test path parsing
+        * @dataProvider provideParse
+        */
+       public function testParse( $patterns, $path, $expected ) {
+               $patterns = (array)$patterns;
+
+               $router = new PathRouter;
+               foreach ( $patterns as $pattern ) {
+                       if ( is_array( $pattern ) ) {
+                               $router->add( $pattern['path'], $pattern['params'] ?? [],
+                                       $pattern['options'] ?? [] );
+                       } else {
+                               $router->add( $pattern );
+                       }
+               }
+               $matches = $router->parse( $path );
+               $this->assertEquals( $matches, $expected );
+       }
+
+       public static function callbackForTest( &$matches, $data ) {
+               $matches['x'] = $data['$1'];
+               $matches['foo'] = $data['foo'];
+       }
+
+       public static function provideWeight() {
+               return [
+                       [ '/Foo', [ 'title' => 'Foo' ] ],
+                       [ '/Bar', [ 'ping' => 'pong' ] ],
+                       [ '/Baz', [ 'marco' => 'polo' ] ],
+                       [ '/asdf-foo', [ 'title' => 'qwerty-foo' ] ],
+                       [ '/qwerty-bar', [ 'title' => 'asdf-bar' ] ],
+                       [ '/a/Foo', [ 'title' => 'Foo' ] ],
+                       [ '/asdf/Foo', [ 'title' => 'Foo' ] ],
+                       [ '/qwerty/Foo', [ 'title' => 'Foo', 'qwerty' => 'qwerty' ] ],
+                       [ '/baz/Foo', [ 'title' => 'Foo', 'unrestricted' => 'baz' ] ],
+                       [ '/y/Foo', [ 'title' => 'Foo', 'restricted-to-y' => 'y' ] ],
+               ];
+       }
+
+       /**
+        * Test to ensure weight of paths is handled correctly
+        * @dataProvider provideWeight
+        */
+       public function testWeight( $path, $expected ) {
+               $router = new PathRouter;
+               $router->addStrict( "/Bar", [ 'ping' => 'pong' ] );
+               $router->add( "/asdf-$1", [ 'title' => 'qwerty-$1' ] );
+               $router->add( "/$1" );
+               $router->add( "/qwerty-$1", [ 'title' => 'asdf-$1' ] );
+               $router->addStrict( "/Baz", [ 'marco' => 'polo' ] );
+               $router->add( "/a/$1" );
+               $router->add( "/asdf/$1" );
+               $router->add( "/$2/$1", [ 'unrestricted' => '$2' ] );
+               $router->add( [ 'qwerty' => "/qwerty/$1" ], [ 'qwerty' => '$key' ] );
+               $router->add( "/$2/$1", [ 'restricted-to-y' => '$2' ], [ '$2' => 'y' ] );
+
+               $this->assertEquals( $router->parse( $path ), $expected );
+       }
+}
diff --git a/tests/phpunit/unit/includes/Revision/FallbackSlotRoleHandlerTest.php b/tests/phpunit/unit/includes/Revision/FallbackSlotRoleHandlerTest.php
new file mode 100644 (file)
index 0000000..9b23c6e
--- /dev/null
@@ -0,0 +1,74 @@
+<?php
+
+namespace MediaWiki\Tests\Revision;
+
+use MediaWiki\Revision\FallbackSlotRoleHandler;
+use Title;
+
+/**
+ * @covers \MediaWiki\Revision\FallbackSlotRoleHandler
+ */
+class FallbackSlotRoleHandlerTest extends \MediaWikiUnitTestCase {
+
+       private function makeBlankTitleObject() {
+               /** @var Title $title */
+               $title = $this->getMockBuilder( Title::class )
+                       ->disableOriginalConstructor()
+                       ->getMock();
+
+               return $title;
+       }
+
+       /**
+        * @covers \MediaWiki\Revision\FallbackSlotRoleHandler::__construct
+        * @covers \MediaWiki\Revision\FallbackSlotRoleHandler::getRole()
+        * @covers \MediaWiki\Revision\FallbackSlotRoleHandler::getNameMessageKey()
+        * @covers \MediaWiki\Revision\FallbackSlotRoleHandler::getDefaultModel()
+        * @covers \MediaWiki\Revision\FallbackSlotRoleHandler::getOutputLayoutHints()
+        */
+       public function testConstruction() {
+               $handler = new FallbackSlotRoleHandler( 'foo' );
+               $this->assertSame( 'foo', $handler->getRole() );
+               $this->assertSame( 'slot-name-foo', $handler->getNameMessageKey() );
+
+               $title = $this->makeBlankTitleObject();
+               $this->assertSame( CONTENT_MODEL_TEXT, $handler->getDefaultModel( $title ) );
+
+               $hints = $handler->getOutputLayoutHints();
+               $this->assertArrayHasKey( 'display', $hints );
+               $this->assertArrayHasKey( 'region', $hints );
+               $this->assertArrayHasKey( 'placement', $hints );
+       }
+
+       /**
+        * @covers \MediaWiki\Revision\FallbackSlotRoleHandler::isAllowedModel()
+        */
+       public function testIsAllowedModel() {
+               $handler = new FallbackSlotRoleHandler( 'foo', 'FooModel' );
+
+               // For the fallback handler, no models are allowed
+               $title = $this->makeBlankTitleObject();
+               $this->assertFalse( $handler->isAllowedModel( 'FooModel', $title ) );
+               $this->assertFalse( $handler->isAllowedModel( 'QuaxModel', $title ) );
+       }
+
+       /**
+        * @covers \MediaWiki\Revision\SlotRoleHandler::isAllowedModel()
+        */
+       public function testIsAllowedOn() {
+               $handler = new FallbackSlotRoleHandler( 'foo', 'FooModel' );
+
+               $title = $this->makeBlankTitleObject();
+               $this->assertFalse( $handler->isAllowedOn( $title ) );
+       }
+
+       /**
+        * @covers \MediaWiki\Revision\FallbackSlotRoleHandler::supportsArticleCount()
+        */
+       public function testSupportsArticleCount() {
+               $handler = new FallbackSlotRoleHandler( 'foo', 'FooModel' );
+
+               $this->assertFalse( $handler->supportsArticleCount() );
+       }
+
+}
diff --git a/tests/phpunit/unit/includes/Revision/MainSlotRoleHandlerTest.php b/tests/phpunit/unit/includes/Revision/MainSlotRoleHandlerTest.php
new file mode 100644 (file)
index 0000000..afd748f
--- /dev/null
@@ -0,0 +1,78 @@
+<?php
+
+namespace MediaWiki\Tests\Revision;
+
+use MediaWiki\Revision\MainSlotRoleHandler;
+use PHPUnit\Framework\MockObject\MockObject;
+use Title;
+
+/**
+ * @covers \MediaWiki\Revision\MainSlotRoleHandler
+ */
+class MainSlotRoleHandlerTest extends \MediaWikiUnitTestCase {
+
+       private function makeTitleObject( $ns ) {
+               /** @var Title|MockObject $title */
+               $title = $this->getMockBuilder( Title::class )
+                       ->disableOriginalConstructor()
+                       ->getMock();
+
+               $title->method( 'getNamespace' )
+                       ->willReturn( $ns );
+
+               return $title;
+       }
+
+       /**
+        * @covers \MediaWiki\Revision\MainSlotRoleHandler::__construct
+        * @covers \MediaWiki\Revision\MainSlotRoleHandler::getRole()
+        * @covers \MediaWiki\Revision\MainSlotRoleHandler::getNameMessageKey()
+        * @covers \MediaWiki\Revision\MainSlotRoleHandler::getOutputLayoutHints()
+        */
+       public function testConstruction() {
+               $handler = new MainSlotRoleHandler( [] );
+               $this->assertSame( 'main', $handler->getRole() );
+               $this->assertSame( 'slot-name-main', $handler->getNameMessageKey() );
+
+               $hints = $handler->getOutputLayoutHints();
+               $this->assertArrayHasKey( 'display', $hints );
+               $this->assertArrayHasKey( 'region', $hints );
+               $this->assertArrayHasKey( 'placement', $hints );
+       }
+
+       /**
+        * @covers \MediaWiki\Revision\MainSlotRoleHandler::getDefaultModel()
+        */
+       public function testFetDefaultModel() {
+               $handler = new MainSlotRoleHandler( [ 100 => CONTENT_MODEL_TEXT ] );
+
+               // For the main handler, the namespace determins the default model
+               $titleMain = $this->makeTitleObject( NS_MAIN );
+               $this->assertSame( CONTENT_MODEL_WIKITEXT, $handler->getDefaultModel( $titleMain ) );
+
+               $title100 = $this->makeTitleObject( 100 );
+               $this->assertSame( CONTENT_MODEL_TEXT, $handler->getDefaultModel( $title100 ) );
+       }
+
+       /**
+        * @covers \MediaWiki\Revision\MainSlotRoleHandler::isAllowedModel()
+        */
+       public function testIsAllowedModel() {
+               $handler = new MainSlotRoleHandler( [] );
+
+               // For the main handler, (nearly) all models are allowed
+               $title = $this->makeTitleObject( NS_MAIN );
+               $this->assertTrue( $handler->isAllowedModel( CONTENT_MODEL_WIKITEXT, $title ) );
+               $this->assertTrue( $handler->isAllowedModel( CONTENT_MODEL_TEXT, $title ) );
+       }
+
+       /**
+        * @covers \MediaWiki\Revision\MainSlotRoleHandler::supportsArticleCount()
+        */
+       public function testSupportsArticleCount() {
+               $handler = new MainSlotRoleHandler( [] );
+
+               $this->assertTrue( $handler->supportsArticleCount() );
+       }
+
+}
diff --git a/tests/phpunit/unit/includes/Revision/RevisionStoreFactoryTest.php b/tests/phpunit/unit/includes/Revision/RevisionStoreFactoryTest.php
new file mode 100644 (file)
index 0000000..7722808
--- /dev/null
@@ -0,0 +1,196 @@
+<?php
+
+namespace MediaWiki\Tests\Revision;
+
+use ActorMigration;
+use CommentStore;
+use MediaWiki\Logger\Spi as LoggerSpi;
+use MediaWiki\Revision\RevisionStore;
+use MediaWiki\Revision\RevisionStoreFactory;
+use MediaWiki\Revision\SlotRoleRegistry;
+use MediaWiki\Storage\BlobStore;
+use MediaWiki\Storage\BlobStoreFactory;
+use MediaWiki\Storage\NameTableStore;
+use MediaWiki\Storage\NameTableStoreFactory;
+use MediaWiki\Storage\SqlBlobStore;
+use Psr\Log\LoggerInterface;
+use Psr\Log\NullLogger;
+use WANObjectCache;
+use Wikimedia\Rdbms\ILBFactory;
+use Wikimedia\Rdbms\ILoadBalancer;
+use Wikimedia\TestingAccessWrapper;
+
+class RevisionStoreFactoryTest extends \MediaWikiUnitTestCase {
+
+       /**
+        * @covers \MediaWiki\Revision\RevisionStoreFactory::__construct
+        */
+       public function testValidConstruction_doesntCauseErrors() {
+               new RevisionStoreFactory(
+                       $this->getMockLoadBalancerFactory(),
+                       $this->getMockBlobStoreFactory(),
+                       $this->getNameTableStoreFactory(),
+                       $this->getMockSlotRoleRegistry(),
+                       $this->getHashWANObjectCache(),
+                       $this->getMockCommentStore(),
+                       ActorMigration::newMigration(),
+                       MIGRATION_OLD,
+                       $this->getMockLoggerSpi(),
+                       true
+               );
+               $this->assertTrue( true );
+       }
+
+       public function provideWikiIds() {
+               yield [ true ];
+               yield [ false ];
+               yield [ 'somewiki' ];
+               yield [ 'somewiki', MIGRATION_OLD , false ];
+               yield [ 'somewiki', MIGRATION_NEW , true ];
+       }
+
+       /**
+        * @dataProvider provideWikiIds
+        * @covers \MediaWiki\Revision\RevisionStoreFactory::getRevisionStore
+        */
+       public function testGetRevisionStore(
+               $wikiId,
+               $mcrMigrationStage = MIGRATION_OLD,
+               $contentHandlerUseDb = true
+       ) {
+               $lbFactory = $this->getMockLoadBalancerFactory();
+               $blobStoreFactory = $this->getMockBlobStoreFactory();
+               $nameTableStoreFactory = $this->getNameTableStoreFactory();
+               $slotRoleRegistry = $this->getMockSlotRoleRegistry();
+               $cache = $this->getHashWANObjectCache();
+               $commentStore = $this->getMockCommentStore();
+               $actorMigration = ActorMigration::newMigration();
+               $loggerProvider = $this->getMockLoggerSpi();
+
+               $factory = new RevisionStoreFactory(
+                       $lbFactory,
+                       $blobStoreFactory,
+                       $nameTableStoreFactory,
+                       $slotRoleRegistry,
+                       $cache,
+                       $commentStore,
+                       $actorMigration,
+                       $mcrMigrationStage,
+                       $loggerProvider,
+                       $contentHandlerUseDb
+               );
+
+               $store = $factory->getRevisionStore( $wikiId );
+               $wrapper = TestingAccessWrapper::newFromObject( $store );
+
+               // ensure the correct object type is returned
+               $this->assertInstanceOf( RevisionStore::class, $store );
+
+               // ensure the RevisionStore is for the given wikiId
+               $this->assertSame( $wikiId, $wrapper->wikiId );
+
+               // ensure all other required services are correctly set
+               $this->assertSame( $cache, $wrapper->cache );
+               $this->assertSame( $commentStore, $wrapper->commentStore );
+               $this->assertSame( $mcrMigrationStage, $wrapper->mcrMigrationStage );
+               $this->assertSame( $actorMigration, $wrapper->actorMigration );
+               $this->assertSame( $contentHandlerUseDb, $store->getContentHandlerUseDB() );
+
+               $this->assertInstanceOf( ILoadBalancer::class, $wrapper->loadBalancer );
+               $this->assertInstanceOf( BlobStore::class, $wrapper->blobStore );
+               $this->assertInstanceOf( NameTableStore::class, $wrapper->contentModelStore );
+               $this->assertInstanceOf( NameTableStore::class, $wrapper->slotRoleStore );
+               $this->assertInstanceOf( LoggerInterface::class, $wrapper->logger );
+       }
+
+       /**
+        * @return \PHPUnit_Framework_MockObject_MockObject|ILoadBalancer
+        */
+       private function getMockLoadBalancer() {
+               return $this->getMockBuilder( ILoadBalancer::class )
+                       ->disableOriginalConstructor()->getMock();
+       }
+
+       /**
+        * @return \PHPUnit_Framework_MockObject_MockObject|ILBFactory
+        */
+       private function getMockLoadBalancerFactory() {
+               $mock = $this->getMockBuilder( ILBFactory::class )
+                       ->disableOriginalConstructor()->getMock();
+
+               $mock->method( 'getMainLB' )
+                       ->willReturnCallback( function () {
+                               return $this->getMockLoadBalancer();
+                       } );
+
+               return $mock;
+       }
+
+       /**
+        * @return \PHPUnit_Framework_MockObject_MockObject|SqlBlobStore
+        */
+       private function getMockSqlBlobStore() {
+               return $this->getMockBuilder( SqlBlobStore::class )
+                       ->disableOriginalConstructor()->getMock();
+       }
+
+       /**
+        * @return \PHPUnit_Framework_MockObject_MockObject|BlobStoreFactory
+        */
+       private function getMockBlobStoreFactory() {
+               $mock = $this->getMockBuilder( BlobStoreFactory::class )
+                       ->disableOriginalConstructor()->getMock();
+
+               $mock->method( 'newSqlBlobStore' )
+                       ->willReturnCallback( function () {
+                               return $this->getMockSqlBlobStore();
+                       } );
+
+               return $mock;
+       }
+
+       /**
+        * @return \PHPUnit_Framework_MockObject_MockObject|SlotRoleRegistry
+        */
+       private function getMockSlotRoleRegistry() {
+               $mock = $this->getMockBuilder( SlotRoleRegistry::class )
+                       ->disableOriginalConstructor()->getMock();
+
+               return $mock;
+       }
+
+       /**
+        * @return NameTableStoreFactory
+        */
+       private function getNameTableStoreFactory() {
+               return new NameTableStoreFactory(
+                       $this->getMockLoadBalancerFactory(),
+                       $this->getHashWANObjectCache(),
+                       new NullLogger() );
+       }
+
+       /**
+        * @return \PHPUnit_Framework_MockObject_MockObject|CommentStore
+        */
+       private function getMockCommentStore() {
+               return $this->getMockBuilder( CommentStore::class )
+                       ->disableOriginalConstructor()->getMock();
+       }
+
+       private function getHashWANObjectCache() {
+               return new WANObjectCache( [ 'cache' => new \HashBagOStuff() ] );
+       }
+
+       /**
+        * @return \PHPUnit_Framework_MockObject_MockObject|LoggerSpi
+        */
+       private function getMockLoggerSpi() {
+               $mock = $this->getMock( LoggerSpi::class );
+
+               $mock->method( 'getLogger' )
+                       ->willReturn( new NullLogger() );
+
+               return $mock;
+       }
+
+}
diff --git a/tests/phpunit/unit/includes/Revision/SlotRecordTest.php b/tests/phpunit/unit/includes/Revision/SlotRecordTest.php
new file mode 100644 (file)
index 0000000..58c1035
--- /dev/null
@@ -0,0 +1,407 @@
+<?php
+
+namespace MediaWiki\Tests\Revision;
+
+use InvalidArgumentException;
+use LogicException;
+use MediaWiki\Revision\IncompleteRevisionException;
+use MediaWiki\Revision\SlotRecord;
+use MediaWiki\Revision\SuppressedDataException;
+use WikitextContent;
+
+/**
+ * @covers \MediaWiki\Revision\SlotRecord
+ */
+class SlotRecordTest extends \MediaWikiUnitTestCase {
+
+       private function makeRow( $data = [] ) {
+               $data = $data + [
+                       'slot_id' => 1234,
+                       'slot_content_id' => 33,
+                       'content_size' => '5',
+                       'content_sha1' => 'someHash',
+                       'content_address' => 'tt:456',
+                       'model_name' => CONTENT_MODEL_WIKITEXT,
+                       'format_name' => CONTENT_FORMAT_WIKITEXT,
+                       'slot_revision_id' => '2',
+                       'slot_origin' => '1',
+                       'role_name' => 'myRole',
+               ];
+               return (object)$data;
+       }
+
+       public function testCompleteConstruction() {
+               $row = $this->makeRow();
+               $record = new SlotRecord( $row, new WikitextContent( 'A' ) );
+
+               $this->assertTrue( $record->hasAddress() );
+               $this->assertTrue( $record->hasContentId() );
+               $this->assertTrue( $record->hasRevision() );
+               $this->assertTrue( $record->isInherited() );
+               $this->assertSame( 'A', $record->getContent()->getText() );
+               $this->assertSame( 5, $record->getSize() );
+               $this->assertSame( 'someHash', $record->getSha1() );
+               $this->assertSame( CONTENT_MODEL_WIKITEXT, $record->getModel() );
+               $this->assertSame( 2, $record->getRevision() );
+               $this->assertSame( 1, $record->getOrigin() );
+               $this->assertSame( 'tt:456', $record->getAddress() );
+               $this->assertSame( 33, $record->getContentId() );
+               $this->assertSame( CONTENT_FORMAT_WIKITEXT, $record->getFormat() );
+               $this->assertSame( 'myRole', $record->getRole() );
+       }
+
+       public function testConstructionDeferred() {
+               $row = $this->makeRow( [
+                       'content_size' => null, // to be computed
+                       'content_sha1' => null, // to be computed
+                       'format_name' => function () {
+                               return CONTENT_FORMAT_WIKITEXT;
+                       },
+                       'slot_revision_id' => '2',
+                       'slot_origin' => '2',
+                       'slot_content_id' => function () {
+                               return null;
+                       },
+               ] );
+
+               $content = function () {
+                       return new WikitextContent( 'A' );
+               };
+
+               $record = new SlotRecord( $row, $content );
+
+               $this->assertTrue( $record->hasAddress() );
+               $this->assertTrue( $record->hasRevision() );
+               $this->assertFalse( $record->hasContentId() );
+               $this->assertFalse( $record->isInherited() );
+               $this->assertSame( 'A', $record->getContent()->getText() );
+               $this->assertSame( 1, $record->getSize() );
+               $this->assertNotNull( $record->getSha1() );
+               $this->assertSame( CONTENT_MODEL_WIKITEXT, $record->getModel() );
+               $this->assertSame( 2, $record->getRevision() );
+               $this->assertSame( 2, $record->getRevision() );
+               $this->assertSame( 'tt:456', $record->getAddress() );
+               $this->assertSame( CONTENT_FORMAT_WIKITEXT, $record->getFormat() );
+               $this->assertSame( 'myRole', $record->getRole() );
+       }
+
+       public function testNewUnsaved() {
+               $record = SlotRecord::newUnsaved( 'myRole', new WikitextContent( 'A' ) );
+
+               $this->assertFalse( $record->hasAddress() );
+               $this->assertFalse( $record->hasContentId() );
+               $this->assertFalse( $record->hasRevision() );
+               $this->assertFalse( $record->isInherited() );
+               $this->assertFalse( $record->hasOrigin() );
+               $this->assertSame( 'A', $record->getContent()->getText() );
+               $this->assertSame( 1, $record->getSize() );
+               $this->assertNotNull( $record->getSha1() );
+               $this->assertSame( CONTENT_MODEL_WIKITEXT, $record->getModel() );
+               $this->assertSame( 'myRole', $record->getRole() );
+       }
+
+       public function provideInvalidConstruction() {
+               yield 'both null' => [ null, null ];
+               yield 'null row' => [ null, new WikitextContent( 'A' ) ];
+               yield 'array row' => [ [], new WikitextContent( 'A' ) ];
+               yield 'empty row' => [ (object)[], new WikitextContent( 'A' ) ];
+               yield 'null content' => [ (object)[], null ];
+       }
+
+       /**
+        * @dataProvider provideInvalidConstruction
+        */
+       public function testInvalidConstruction( $row, $content ) {
+               $this->setExpectedException( InvalidArgumentException::class );
+               new SlotRecord( $row, $content );
+       }
+
+       public function testGetContentId_fails() {
+               $record = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
+               $this->setExpectedException( IncompleteRevisionException::class );
+
+               $record->getContentId();
+       }
+
+       public function testGetAddress_fails() {
+               $record = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
+               $this->setExpectedException( IncompleteRevisionException::class );
+
+               $record->getAddress();
+       }
+
+       public function provideIncomplete() {
+               $unsaved = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
+               yield 'unsaved' => [ $unsaved ];
+
+               $parent = new SlotRecord( $this->makeRow(), new WikitextContent( 'A' ) );
+               $inherited = SlotRecord::newInherited( $parent );
+               yield 'inherited' => [ $inherited ];
+       }
+
+       /**
+        * @dataProvider provideIncomplete
+        */
+       public function testGetRevision_fails( SlotRecord $record ) {
+               $record = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
+               $this->setExpectedException( IncompleteRevisionException::class );
+
+               $record->getRevision();
+       }
+
+       /**
+        * @dataProvider provideIncomplete
+        */
+       public function testGetOrigin_fails( SlotRecord $record ) {
+               $record = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
+               $this->setExpectedException( IncompleteRevisionException::class );
+
+               $record->getOrigin();
+       }
+
+       public function provideHashStability() {
+               yield [ '', 'phoiac9h4m842xq45sp7s6u21eteeq1' ];
+               yield [ 'Lorem ipsum', 'hcr5u40uxr81d3nx89nvwzclfz6r9c5' ];
+       }
+
+       /**
+        * @dataProvider provideHashStability
+        */
+       public function testHashStability( $text, $hash ) {
+               // Changing the output of the hash function will break things horribly!
+
+               $this->assertSame( $hash, SlotRecord::base36Sha1( $text ) );
+
+               $record = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( $text ) );
+               $this->assertSame( $hash, $record->getSha1() );
+       }
+
+       public function testNewWithSuppressedContent() {
+               $input = new SlotRecord( $this->makeRow(), new WikitextContent( 'A' ) );
+               $output = SlotRecord::newWithSuppressedContent( $input );
+
+               $this->setExpectedException( SuppressedDataException::class );
+               $output->getContent();
+       }
+
+       public function testNewInherited() {
+               $row = $this->makeRow( [ 'slot_revision_id' => 7, 'slot_origin' => 7 ] );
+               $parent = new SlotRecord( $row, new WikitextContent( 'A' ) );
+
+               // This would happen while doing an edit, before saving revision meta-data.
+               $inherited = SlotRecord::newInherited( $parent );
+
+               $this->assertSame( $parent->getContentId(), $inherited->getContentId() );
+               $this->assertSame( $parent->getAddress(), $inherited->getAddress() );
+               $this->assertSame( $parent->getContent(), $inherited->getContent() );
+               $this->assertTrue( $inherited->isInherited() );
+               $this->assertTrue( $inherited->hasOrigin() );
+               $this->assertFalse( $inherited->hasRevision() );
+
+               // make sure we didn't mess with the internal state of $parent
+               $this->assertFalse( $parent->isInherited() );
+               $this->assertSame( 7, $parent->getRevision() );
+
+               // This would happen while doing an edit, after saving the revision meta-data
+               // and content meta-data.
+               $saved = SlotRecord::newSaved(
+                       10,
+                       $inherited->getContentId(),
+                       $inherited->getAddress(),
+                       $inherited
+               );
+               $this->assertSame( $parent->getContentId(), $saved->getContentId() );
+               $this->assertSame( $parent->getAddress(), $saved->getAddress() );
+               $this->assertSame( $parent->getContent(), $saved->getContent() );
+               $this->assertTrue( $saved->isInherited() );
+               $this->assertTrue( $saved->hasRevision() );
+               $this->assertSame( 10, $saved->getRevision() );
+
+               // make sure we didn't mess with the internal state of $parent or $inherited
+               $this->assertSame( 7, $parent->getRevision() );
+               $this->assertFalse( $inherited->hasRevision() );
+       }
+
+       public function testNewSaved() {
+               // This would happen while doing an edit, before saving revision meta-data.
+               $unsaved = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
+
+               // This would happen while doing an edit, after saving the revision meta-data
+               // and content meta-data.
+               $saved = SlotRecord::newSaved( 10, 20, 'theNewAddress', $unsaved );
+               $this->assertFalse( $saved->isInherited() );
+               $this->assertTrue( $saved->hasOrigin() );
+               $this->assertTrue( $saved->hasRevision() );
+               $this->assertTrue( $saved->hasAddress() );
+               $this->assertTrue( $saved->hasContentId() );
+               $this->assertSame( 'theNewAddress', $saved->getAddress() );
+               $this->assertSame( 20, $saved->getContentId() );
+               $this->assertSame( 'A', $saved->getContent()->getText() );
+               $this->assertSame( 10, $saved->getRevision() );
+               $this->assertSame( 10, $saved->getOrigin() );
+
+               // make sure we didn't mess with the internal state of $unsaved
+               $this->assertFalse( $unsaved->hasAddress() );
+               $this->assertFalse( $unsaved->hasContentId() );
+               $this->assertFalse( $unsaved->hasRevision() );
+       }
+
+       public function provideNewSaved_LogicException() {
+               $freshRow = $this->makeRow( [
+                       'content_id' => 10,
+                       'content_address' => 'address:1',
+                       'slot_origin' => 1,
+                       'slot_revision_id' => 1,
+               ] );
+
+               $freshSlot = new SlotRecord( $freshRow, new WikitextContent( 'A' ) );
+               yield 'mismatching address' => [ 1, 10, 'address:BAD', $freshSlot ];
+               yield 'mismatching revision' => [ 5, 10, 'address:1', $freshSlot ];
+               yield 'mismatching content ID' => [ 1, 17, 'address:1', $freshSlot ];
+
+               $inheritedRow = $this->makeRow( [
+                       'content_id' => null,
+                       'content_address' => null,
+                       'slot_origin' => 0,
+                       'slot_revision_id' => 1,
+               ] );
+
+               $inheritedSlot = new SlotRecord( $inheritedRow, new WikitextContent( 'A' ) );
+               yield 'inherited, but no address' => [ 1, 10, 'address:2', $inheritedSlot ];
+       }
+
+       /**
+        * @dataProvider provideNewSaved_LogicException
+        */
+       public function testNewSaved_LogicException(
+               $revisionId,
+               $contentId,
+               $contentAddress,
+               SlotRecord $protoSlot
+       ) {
+               $this->setExpectedException( LogicException::class );
+               SlotRecord::newSaved( $revisionId, $contentId, $contentAddress, $protoSlot );
+       }
+
+       public function provideNewSaved_InvalidArgumentException() {
+               $unsaved = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
+
+               yield 'bad revision id' => [ 'xyzzy', 5, 'address', $unsaved ];
+               yield 'bad content id' => [ 7, 'xyzzy', 'address', $unsaved ];
+               yield 'bad content address' => [ 7, 5, 77, $unsaved ];
+       }
+
+       /**
+        * @dataProvider provideNewSaved_InvalidArgumentException
+        */
+       public function testNewSaved_InvalidArgumentException(
+               $revisionId,
+               $contentId,
+               $contentAddress,
+               SlotRecord $protoSlot
+       ) {
+               $this->setExpectedException( InvalidArgumentException::class );
+               SlotRecord::newSaved( $revisionId, $contentId, $contentAddress, $protoSlot );
+       }
+
+       public function provideHasSameContent() {
+               $fail = function () {
+                       self::fail( 'There should be no need to actually load the content.' );
+               };
+
+               $a100a1 = new SlotRecord(
+                       $this->makeRow(
+                               [
+                                       'model_name' => 'A',
+                                       'content_size' => 100,
+                                       'content_sha1' => 'hash-a',
+                                       'content_address' => 'xxx:a1',
+                               ]
+                       ),
+                       $fail
+               );
+               $a100a1b = new SlotRecord(
+                       $this->makeRow(
+                               [
+                                       'model_name' => 'A',
+                                       'content_size' => 100,
+                                       'content_sha1' => 'hash-a',
+                                       'content_address' => 'xxx:a1',
+                               ]
+                       ),
+                       $fail
+               );
+               $a100null = new SlotRecord(
+                       $this->makeRow(
+                               [
+                                       'model_name' => 'A',
+                                       'content_size' => 100,
+                                       'content_sha1' => 'hash-a',
+                                       'content_address' => null,
+                               ]
+                       ),
+                       $fail
+               );
+               $a100a2 = new SlotRecord(
+                       $this->makeRow(
+                               [
+                                       'model_name' => 'A',
+                                       'content_size' => 100,
+                                       'content_sha1' => 'hash-a',
+                                       'content_address' => 'xxx:a2',
+                               ]
+                       ),
+                       $fail
+               );
+               $b100a1 = new SlotRecord(
+                       $this->makeRow(
+                               [
+                                       'model_name' => 'B',
+                                       'content_size' => 100,
+                                       'content_sha1' => 'hash-a',
+                                       'content_address' => 'xxx:a1',
+                               ]
+                       ),
+                       $fail
+               );
+               $a200a1 = new SlotRecord(
+                       $this->makeRow(
+                               [
+                                       'model_name' => 'A',
+                                       'content_size' => 200,
+                                       'content_sha1' => 'hash-a',
+                                       'content_address' => 'xxx:a2',
+                               ]
+                       ),
+                       $fail
+               );
+               $a100x1 = new SlotRecord(
+                       $this->makeRow(
+                               [
+                                       'model_name' => 'A',
+                                       'content_size' => 100,
+                                       'content_sha1' => 'hash-x',
+                                       'content_address' => 'xxx:x1',
+                               ]
+                       ),
+                       $fail
+               );
+
+               yield 'same instance' => [ $a100a1, $a100a1, true ];
+               yield 'no address' => [ $a100a1, $a100null, true ];
+               yield 'same address' => [ $a100a1, $a100a1b, true ];
+               yield 'different address' => [ $a100a1, $a100a2, true ];
+               yield 'different model' => [ $a100a1, $b100a1, false ];
+               yield 'different size' => [ $a100a1, $a200a1, false ];
+               yield 'different hash' => [ $a100a1, $a100x1, false ];
+       }
+
+       /**
+        * @dataProvider provideHasSameContent
+        */
+       public function testHasSameContent( SlotRecord $a, SlotRecord $b, $sameContent ) {
+               $this->assertSame( $sameContent, $a->hasSameContent( $b ) );
+               $this->assertSame( $sameContent, $b->hasSameContent( $a ) );
+       }
+
+}
diff --git a/tests/phpunit/unit/includes/Revision/SlotRoleHandlerTest.php b/tests/phpunit/unit/includes/Revision/SlotRoleHandlerTest.php
new file mode 100644 (file)
index 0000000..ed3053c
--- /dev/null
@@ -0,0 +1,66 @@
+<?php
+
+namespace MediaWiki\Tests\Revision;
+
+use MediaWiki\Revision\SlotRoleHandler;
+use Title;
+
+/**
+ * @covers \MediaWiki\Revision\SlotRoleHandler
+ */
+class SlotRoleHandlerTest extends \MediaWikiUnitTestCase {
+
+       private function makeBlankTitleObject() {
+               /** @var Title $title */
+               $title = $this->getMockBuilder( Title::class )
+                       ->disableOriginalConstructor()
+                       ->getMock();
+
+               return $title;
+       }
+
+       /**
+        * @covers \MediaWiki\Revision\SlotRoleHandler::__construct
+        * @covers \MediaWiki\Revision\SlotRoleHandler::getRole()
+        * @covers \MediaWiki\Revision\SlotRoleHandler::getNameMessageKey()
+        * @covers \MediaWiki\Revision\SlotRoleHandler::getDefaultModel()
+        * @covers \MediaWiki\Revision\SlotRoleHandler::getOutputLayoutHints()
+        */
+       public function testConstruction() {
+               $handler = new SlotRoleHandler( 'foo', 'FooModel', [ 'frob' => 'niz' ] );
+               $this->assertSame( 'foo', $handler->getRole() );
+               $this->assertSame( 'slot-name-foo', $handler->getNameMessageKey() );
+
+               $title = $this->makeBlankTitleObject();
+               $this->assertSame( 'FooModel', $handler->getDefaultModel( $title ) );
+
+               $hints = $handler->getOutputLayoutHints();
+               $this->assertArrayHasKey( 'frob', $hints );
+               $this->assertSame( 'niz', $hints['frob'] );
+
+               $this->assertArrayHasKey( 'display', $hints );
+               $this->assertArrayHasKey( 'region', $hints );
+               $this->assertArrayHasKey( 'placement', $hints );
+       }
+
+       /**
+        * @covers \MediaWiki\Revision\SlotRoleHandler::isAllowedModel()
+        */
+       public function testIsAllowedModel() {
+               $handler = new SlotRoleHandler( 'foo', 'FooModel' );
+
+               $title = $this->makeBlankTitleObject();
+               $this->assertTrue( $handler->isAllowedModel( 'FooModel', $title ) );
+               $this->assertFalse( $handler->isAllowedModel( 'QuaxModel', $title ) );
+       }
+
+       /**
+        * @covers \MediaWiki\Revision\SlotRoleHandler::supportsArticleCount()
+        */
+       public function testSupportsArticleCount() {
+               $handler = new SlotRoleHandler( 'foo', 'FooModel' );
+
+               $this->assertFalse( $handler->supportsArticleCount() );
+       }
+
+}
diff --git a/tests/phpunit/unit/includes/SanitizerValidateEmailTest.php b/tests/phpunit/unit/includes/SanitizerValidateEmailTest.php
new file mode 100644 (file)
index 0000000..c4e4308
--- /dev/null
@@ -0,0 +1,105 @@
+<?php
+
+/**
+ * @covers Sanitizer::validateEmail
+ * @todo all test methods in this class should be refactored and...
+ *    use a single test method and a single data provider...
+ */
+class SanitizerValidateEmailTest extends PHPUnit\Framework\TestCase {
+
+       use MediaWikiCoversValidator;
+
+       private function checkEmail( $addr, $expected = true, $msg = '' ) {
+               if ( $msg == '' ) {
+                       $msg = "Testing $addr";
+               }
+
+               $this->assertEquals(
+                       $expected,
+                       Sanitizer::validateEmail( $addr ),
+                       $msg
+               );
+       }
+
+       private function valid( $addr, $msg = '' ) {
+               $this->checkEmail( $addr, true, $msg );
+       }
+
+       private function invalid( $addr, $msg = '' ) {
+               $this->checkEmail( $addr, false, $msg );
+       }
+
+       public function testEmailWellKnownUserAtHostDotTldAreValid() {
+               $this->valid( 'user@example.com' );
+               $this->valid( 'user@example.museum' );
+       }
+
+       public function testEmailWithUpperCaseCharactersAreValid() {
+               $this->valid( 'USER@example.com' );
+               $this->valid( 'user@EXAMPLE.COM' );
+               $this->valid( 'user@Example.com' );
+               $this->valid( 'USER@eXAMPLE.com' );
+       }
+
+       public function testEmailWithAPlusInUserName() {
+               $this->valid( 'user+sub@example.com' );
+               $this->valid( 'user+@example.com' );
+       }
+
+       public function testEmailDoesNotNeedATopLevelDomain() {
+               $this->valid( "user@localhost" );
+               $this->valid( "FooBar@localdomain" );
+               $this->valid( "nobody@mycompany" );
+       }
+
+       public function testEmailWithWhiteSpacesBeforeOrAfterAreInvalids() {
+               $this->invalid( " user@host.com" );
+               $this->invalid( "user@host.com " );
+               $this->invalid( "\tuser@host.com" );
+               $this->invalid( "user@host.com\t" );
+       }
+
+       public function testEmailWithWhiteSpacesAreInvalids() {
+               $this->invalid( "User user@host" );
+               $this->invalid( "first last@mycompany" );
+               $this->invalid( "firstlast@my company" );
+       }
+
+       /**
+        * T28948 : comma were matched by an incorrect regexp range
+        */
+       public function testEmailWithCommasAreInvalids() {
+               $this->invalid( "user,foo@example.org" );
+               $this->invalid( "userfoo@ex,ample.org" );
+       }
+
+       public function testEmailWithHyphens() {
+               $this->valid( "user-foo@example.org" );
+               $this->valid( "userfoo@ex-ample.org" );
+       }
+
+       public function testEmailDomainCanNotBeginWithDot() {
+               $this->invalid( "user@." );
+               $this->invalid( "user@.localdomain" );
+               $this->invalid( "user@localdomain." );
+               $this->valid( "user.@localdomain" );
+               $this->valid( ".@localdomain" );
+               $this->invalid( ".@a............" );
+       }
+
+       public function testEmailWithFunnyCharacters() {
+               $this->valid( "\$user!ex{this}@123.com" );
+       }
+
+       public function testEmailTopLevelDomainCanBeNumerical() {
+               $this->valid( "user@example.1234" );
+       }
+
+       public function testEmailWithoutAtSignIsInvalid() {
+               $this->invalid( 'useràexample.com' );
+       }
+
+       public function testEmailWithOneCharacterDomainIsValid() {
+               $this->valid( 'user@a' );
+       }
+}
diff --git a/tests/phpunit/unit/includes/ServiceWiringTest.php b/tests/phpunit/unit/includes/ServiceWiringTest.php
new file mode 100644 (file)
index 0000000..25b0214
--- /dev/null
@@ -0,0 +1,16 @@
+<?php
+
+/**
+ * @coversNothing
+ */
+class ServiceWiringTest extends \MediaWikiUnitTestCase {
+       public function testServicesAreSorted() {
+               global $IP;
+               $services = array_keys( require "$IP/includes/ServiceWiring.php" );
+               $sortedServices = $services;
+               natcasesort( $sortedServices );
+
+               $this->assertSame( $sortedServices, $services,
+                       'Please keep services sorted alphabetically' );
+       }
+}
diff --git a/tests/phpunit/unit/includes/SiteConfigurationTest.php b/tests/phpunit/unit/includes/SiteConfigurationTest.php
new file mode 100644 (file)
index 0000000..b992a86
--- /dev/null
@@ -0,0 +1,379 @@
+<?php
+
+class SiteConfigurationTest extends \MediaWikiUnitTestCase {
+
+       /**
+        * @var SiteConfiguration
+        */
+       protected $mConf;
+
+       protected function setUp() {
+               parent::setUp();
+
+               $this->mConf = new SiteConfiguration;
+
+               $this->mConf->suffixes = [ 'wikipedia' => 'wiki' ];
+               $this->mConf->wikis = [ 'enwiki', 'dewiki', 'frwiki' ];
+               $this->mConf->settings = [
+                       'SimpleKey' => [
+                               'wiki' => 'wiki',
+                               'tag' => 'tag',
+                               'enwiki' => 'enwiki',
+                               'dewiki' => 'dewiki',
+                               'frwiki' => 'frwiki',
+                       ],
+
+                       'Fallback' => [
+                               'default' => 'default',
+                               'wiki' => 'wiki',
+                               'tag' => 'tag',
+                               'frwiki' => 'frwiki',
+                               'null_wiki' => null,
+                       ],
+
+                       'WithParams' => [
+                               'default' => '$lang $site $wiki',
+                       ],
+
+                       '+SomeGlobal' => [
+                               'wiki' => [
+                                       'wiki' => 'wiki',
+                               ],
+                               'tag' => [
+                                       'tag' => 'tag',
+                               ],
+                               'enwiki' => [
+                                       'enwiki' => 'enwiki',
+                               ],
+                               'dewiki' => [
+                                       'dewiki' => 'dewiki',
+                               ],
+                               'frwiki' => [
+                                       'frwiki' => 'frwiki',
+                               ],
+                       ],
+
+                       'MergeIt' => [
+                               '+wiki' => [
+                                       'wiki' => 'wiki',
+                               ],
+                               '+tag' => [
+                                       'tag' => 'tag',
+                               ],
+                               'default' => [
+                                       'default' => 'default',
+                               ],
+                               '+enwiki' => [
+                                       'enwiki' => 'enwiki',
+                               ],
+                               '+dewiki' => [
+                                       'dewiki' => 'dewiki',
+                               ],
+                               '+frwiki' => [
+                                       'frwiki' => 'frwiki',
+                               ],
+                       ],
+               ];
+
+               $GLOBALS['SomeGlobal'] = [ 'SomeGlobal' => 'SomeGlobal' ];
+       }
+
+       /**
+        * This function is used as a callback within the tests below
+        */
+       public static function getSiteParamsCallback( $conf, $wiki ) {
+               $site = null;
+               $lang = null;
+               foreach ( $conf->suffixes as $suffix ) {
+                       if ( substr( $wiki, -strlen( $suffix ) ) == $suffix ) {
+                               $site = $suffix;
+                               $lang = substr( $wiki, 0, -strlen( $suffix ) );
+                               break;
+                       }
+               }
+
+               return [
+                       'suffix' => $site,
+                       'lang' => $lang,
+                       'params' => [
+                               'lang' => $lang,
+                               'site' => $site,
+                               'wiki' => $wiki,
+                       ],
+                       'tags' => [ 'tag' ],
+               ];
+       }
+
+       /**
+        * @covers SiteConfiguration::siteFromDB
+        */
+       public function testSiteFromDb() {
+               $this->assertEquals(
+                       [ 'wikipedia', 'en' ],
+                       $this->mConf->siteFromDB( 'enwiki' ),
+                       'siteFromDB()'
+               );
+               $this->assertEquals(
+                       [ 'wikipedia', '' ],
+                       $this->mConf->siteFromDB( 'wiki' ),
+                       'siteFromDB() on a suffix'
+               );
+               $this->assertEquals(
+                       [ null, null ],
+                       $this->mConf->siteFromDB( 'wikien' ),
+                       'siteFromDB() on a non-existing wiki'
+               );
+
+               $this->mConf->suffixes = [ 'wiki', '' ];
+               $this->assertEquals(
+                       [ '', 'wikien' ],
+                       $this->mConf->siteFromDB( 'wikien' ),
+                       'siteFromDB() on a non-existing wiki (2)'
+               );
+       }
+
+       /**
+        * @covers SiteConfiguration::getLocalDatabases
+        */
+       public function testGetLocalDatabases() {
+               $this->assertEquals(
+                       [ 'enwiki', 'dewiki', 'frwiki' ],
+                       $this->mConf->getLocalDatabases(),
+                       'getLocalDatabases()'
+               );
+       }
+
+       /**
+        * @covers SiteConfiguration::get
+        */
+       public function testGetConfVariables() {
+               // Simple
+               $this->assertEquals(
+                       'enwiki',
+                       $this->mConf->get( 'SimpleKey', 'enwiki', 'wiki' ),
+                       'get(): simple setting on an existing wiki'
+               );
+               $this->assertEquals(
+                       'dewiki',
+                       $this->mConf->get( 'SimpleKey', 'dewiki', 'wiki' ),
+                       'get(): simple setting on an existing wiki (2)'
+               );
+               $this->assertEquals(
+                       'frwiki',
+                       $this->mConf->get( 'SimpleKey', 'frwiki', 'wiki' ),
+                       'get(): simple setting on an existing wiki (3)'
+               );
+               $this->assertEquals(
+                       'wiki',
+                       $this->mConf->get( 'SimpleKey', 'wiki', 'wiki' ),
+                       'get(): simple setting on an suffix'
+               );
+               $this->assertEquals(
+                       'wiki',
+                       $this->mConf->get( 'SimpleKey', 'eswiki', 'wiki' ),
+                       'get(): simple setting on an non-existing wiki'
+               );
+
+               // Fallback
+               $this->assertEquals(
+                       'wiki',
+                       $this->mConf->get( 'Fallback', 'enwiki', 'wiki' ),
+                       'get(): fallback setting on an existing wiki'
+               );
+               $this->assertEquals(
+                       'tag',
+                       $this->mConf->get( 'Fallback', 'dewiki', 'wiki', [], [ 'tag' ] ),
+                       'get(): fallback setting on an existing wiki (with wiki tag)'
+               );
+               $this->assertEquals(
+                       'frwiki',
+                       $this->mConf->get( 'Fallback', 'frwiki', 'wiki', [], [ 'tag' ] ),
+                       'get(): no fallback if wiki has its own setting (matching tag)'
+               );
+               $this->assertSame(
+                       // Potential regression test for T192855
+                       null,
+                       $this->mConf->get( 'Fallback', 'null_wiki', 'wiki', [], [ 'tag' ] ),
+                       'get(): no fallback if wiki has its own setting (matching tag and uses null)'
+               );
+               $this->assertEquals(
+                       'wiki',
+                       $this->mConf->get( 'Fallback', 'wiki', 'wiki' ),
+                       'get(): fallback setting on an suffix'
+               );
+               $this->assertEquals(
+                       'wiki',
+                       $this->mConf->get( 'Fallback', 'wiki', 'wiki', [], [ 'tag' ] ),
+                       'get(): fallback setting on an suffix (with wiki tag)'
+               );
+               $this->assertEquals(
+                       'wiki',
+                       $this->mConf->get( 'Fallback', 'eswiki', 'wiki' ),
+                       'get(): fallback setting on an non-existing wiki'
+               );
+               $this->assertEquals(
+                       'tag',
+                       $this->mConf->get( 'Fallback', 'eswiki', 'wiki', [], [ 'tag' ] ),
+                       'get(): fallback setting on an non-existing wiki (with wiki tag)'
+               );
+
+               // Merging
+               $common = [ 'wiki' => 'wiki', 'default' => 'default' ];
+               $commonTag = [ 'tag' => 'tag', 'wiki' => 'wiki', 'default' => 'default' ];
+               $this->assertEquals(
+                       [ 'enwiki' => 'enwiki' ] + $common,
+                       $this->mConf->get( 'MergeIt', 'enwiki', 'wiki' ),
+                       'get(): merging setting on an existing wiki'
+               );
+               $this->assertEquals(
+                       [ 'enwiki' => 'enwiki' ] + $commonTag,
+                       $this->mConf->get( 'MergeIt', 'enwiki', 'wiki', [], [ 'tag' ] ),
+                       'get(): merging setting on an existing wiki (with tag)'
+               );
+               $this->assertEquals(
+                       [ 'dewiki' => 'dewiki' ] + $common,
+                       $this->mConf->get( 'MergeIt', 'dewiki', 'wiki' ),
+                       'get(): merging setting on an existing wiki (2)'
+               );
+               $this->assertEquals(
+                       [ 'dewiki' => 'dewiki' ] + $commonTag,
+                       $this->mConf->get( 'MergeIt', 'dewiki', 'wiki', [], [ 'tag' ] ),
+                       'get(): merging setting on an existing wiki (2) (with tag)'
+               );
+               $this->assertEquals(
+                       [ 'frwiki' => 'frwiki' ] + $common,
+                       $this->mConf->get( 'MergeIt', 'frwiki', 'wiki' ),
+                       'get(): merging setting on an existing wiki (3)'
+               );
+               $this->assertEquals(
+                       [ 'frwiki' => 'frwiki' ] + $commonTag,
+                       $this->mConf->get( 'MergeIt', 'frwiki', 'wiki', [], [ 'tag' ] ),
+                       'get(): merging setting on an existing wiki (3) (with tag)'
+               );
+               $this->assertEquals(
+                       [ 'wiki' => 'wiki' ] + $common,
+                       $this->mConf->get( 'MergeIt', 'wiki', 'wiki' ),
+                       'get(): merging setting on an suffix'
+               );
+               $this->assertEquals(
+                       [ 'wiki' => 'wiki' ] + $commonTag,
+                       $this->mConf->get( 'MergeIt', 'wiki', 'wiki', [], [ 'tag' ] ),
+                       'get(): merging setting on an suffix (with tag)'
+               );
+               $this->assertEquals(
+                       $common,
+                       $this->mConf->get( 'MergeIt', 'eswiki', 'wiki' ),
+                       'get(): merging setting on an non-existing wiki'
+               );
+               $this->assertEquals(
+                       $commonTag,
+                       $this->mConf->get( 'MergeIt', 'eswiki', 'wiki', [], [ 'tag' ] ),
+                       'get(): merging setting on an non-existing wiki (with tag)'
+               );
+       }
+
+       /**
+        * @covers SiteConfiguration::siteFromDB
+        */
+       public function testSiteFromDbWithCallback() {
+               $this->mConf->siteParamsCallback = 'SiteConfigurationTest::getSiteParamsCallback';
+
+               $this->assertEquals(
+                       [ 'wiki', 'en' ],
+                       $this->mConf->siteFromDB( 'enwiki' ),
+                       'siteFromDB() with callback'
+               );
+               $this->assertEquals(
+                       [ 'wiki', '' ],
+                       $this->mConf->siteFromDB( 'wiki' ),
+                       'siteFromDB() with callback on a suffix'
+               );
+               $this->assertEquals(
+                       [ null, null ],
+                       $this->mConf->siteFromDB( 'wikien' ),
+                       'siteFromDB() with callback on a non-existing wiki'
+               );
+       }
+
+       /**
+        * @covers SiteConfiguration::get
+        */
+       public function testParameterReplacement() {
+               $this->mConf->siteParamsCallback = 'SiteConfigurationTest::getSiteParamsCallback';
+
+               $this->assertEquals(
+                       'en wiki enwiki',
+                       $this->mConf->get( 'WithParams', 'enwiki', 'wiki' ),
+                       'get(): parameter replacement on an existing wiki'
+               );
+               $this->assertEquals(
+                       'de wiki dewiki',
+                       $this->mConf->get( 'WithParams', 'dewiki', 'wiki' ),
+                       'get(): parameter replacement on an existing wiki (2)'
+               );
+               $this->assertEquals(
+                       'fr wiki frwiki',
+                       $this->mConf->get( 'WithParams', 'frwiki', 'wiki' ),
+                       'get(): parameter replacement on an existing wiki (3)'
+               );
+               $this->assertEquals(
+                       ' wiki wiki',
+                       $this->mConf->get( 'WithParams', 'wiki', 'wiki' ),
+                       'get(): parameter replacement on an suffix'
+               );
+               $this->assertEquals(
+                       'es wiki eswiki',
+                       $this->mConf->get( 'WithParams', 'eswiki', 'wiki' ),
+                       'get(): parameter replacement on an non-existing wiki'
+               );
+       }
+
+       /**
+        * @covers SiteConfiguration::getAll
+        */
+       public function testGetAllGlobals() {
+               $this->mConf->siteParamsCallback = 'SiteConfigurationTest::getSiteParamsCallback';
+
+               $getall = [
+                       'SimpleKey' => 'enwiki',
+                       'Fallback' => 'tag',
+                       'WithParams' => 'en wiki enwiki',
+                       'SomeGlobal' => [ 'enwiki' => 'enwiki' ] + $GLOBALS['SomeGlobal'],
+                       'MergeIt' => [
+                               'enwiki' => 'enwiki',
+                               'tag' => 'tag',
+                               'wiki' => 'wiki',
+                               'default' => 'default'
+                       ],
+               ];
+               $this->assertEquals( $getall, $this->mConf->getAll( 'enwiki' ), 'getAll()' );
+
+               $this->mConf->extractAllGlobals( 'enwiki', 'wiki' );
+
+               $this->assertEquals(
+                       $getall['SimpleKey'],
+                       $GLOBALS['SimpleKey'],
+                       'extractAllGlobals(): simple setting'
+               );
+               $this->assertEquals(
+                       $getall['Fallback'],
+                       $GLOBALS['Fallback'],
+                       'extractAllGlobals(): fallback setting'
+               );
+               $this->assertEquals(
+                       $getall['WithParams'],
+                       $GLOBALS['WithParams'],
+                       'extractAllGlobals(): parameter replacement'
+               );
+               $this->assertEquals(
+                       $getall['SomeGlobal'],
+                       $GLOBALS['SomeGlobal'],
+                       'extractAllGlobals(): merging with global'
+               );
+               $this->assertEquals(
+                       $getall['MergeIt'],
+                       $GLOBALS['MergeIt'],
+                       'extractAllGlobals(): merging setting'
+               );
+       }
+}
diff --git a/tests/phpunit/unit/includes/Storage/BlobStoreFactoryTest.php b/tests/phpunit/unit/includes/Storage/BlobStoreFactoryTest.php
new file mode 100644 (file)
index 0000000..a94214f
--- /dev/null
@@ -0,0 +1,70 @@
+<?php
+
+namespace MediaWiki\Tests\Storage;
+
+use MediaWiki\MediaWikiServices;
+use MediaWiki\Storage\BlobStore;
+use MediaWiki\Storage\SqlBlobStore;
+use Wikimedia\Rdbms\LBFactory;
+use Wikimedia\TestingAccessWrapper;
+
+/**
+ * @covers \MediaWiki\Storage\BlobStoreFactory
+ */
+class BlobStoreFactoryTest extends \MediaWikiUnitTestCase {
+
+       /** @var LBFactory|\PHPUnit_Framework_MockObject_MockObject $lbFactoryMock */
+       private $lbFactoryMock;
+
+       protected function setUp() {
+               parent::setUp();
+
+               $this->lbFactoryMock = $this->createMock( LBFactory::class );
+
+               $lbFactoryMockProvider = function (): LBFactory {
+                       return $this->lbFactoryMock;
+               };
+
+               $this->overrideMwServices( [ 'DBLoadBalancerFactory' => $lbFactoryMockProvider ] );
+       }
+
+       public function provideWikiIds() {
+               yield [ false ];
+               yield [ 'someWiki' ];
+       }
+
+       /**
+        * @dataProvider provideWikiIds
+        */
+       public function testNewBlobStore( $wikiId ) {
+               $this->lbFactoryMock->expects( $this->any() )
+                       ->method( 'getMainLB' )
+                       ->with( $wikiId )
+                       ->willReturn( $this->createMock( \LoadBalancer::class ) );
+
+               $factory = MediaWikiServices::getInstance()->getBlobStoreFactory();
+               $store = $factory->newBlobStore( $wikiId );
+               $this->assertInstanceOf( BlobStore::class, $store );
+
+               // This only works as we currently know this is a SqlBlobStore object
+               $wrapper = TestingAccessWrapper::newFromObject( $store );
+               $this->assertEquals( $wikiId, $wrapper->wikiId );
+       }
+
+       /**
+        * @dataProvider provideWikiIds
+        */
+       public function testNewSqlBlobStore( $wikiId ) {
+               $this->lbFactoryMock->expects( $this->any() )
+                       ->method( 'getMainLB' )
+                       ->with( $wikiId )
+                       ->willReturn( $this->createMock( \LoadBalancer::class ) );
+
+               $factory = MediaWikiServices::getInstance()->getBlobStoreFactory();
+               $store = $factory->newSqlBlobStore( $wikiId );
+               $this->assertInstanceOf( SqlBlobStore::class, $store );
+
+               $wrapper = TestingAccessWrapper::newFromObject( $store );
+               $this->assertEquals( $wikiId, $wrapper->wikiId );
+       }
+}
diff --git a/tests/phpunit/unit/includes/Storage/PreparedEditTest.php b/tests/phpunit/unit/includes/Storage/PreparedEditTest.php
new file mode 100644 (file)
index 0000000..e3249e7
--- /dev/null
@@ -0,0 +1,21 @@
+<?php
+
+namespace MediaWiki\Edit;
+
+use ParserOutput;
+
+/**
+ * @covers \MediaWiki\Edit\PreparedEdit
+ */
+class PreparedEditTest extends \MediaWikiUnitTestCase {
+       function testCallback() {
+               $output = new ParserOutput();
+               $edit = new PreparedEdit();
+               $edit->parserOutputCallback = function () {
+                       return new ParserOutput();
+               };
+
+               $this->assertEquals( $output, $edit->getOutput() );
+               $this->assertEquals( $output, $edit->output );
+       }
+}
diff --git a/tests/phpunit/unit/includes/TitleArrayFromResultTest.php b/tests/phpunit/unit/includes/TitleArrayFromResultTest.php
new file mode 100644 (file)
index 0000000..32c7571
--- /dev/null
@@ -0,0 +1,117 @@
+<?php
+
+/**
+ * @author Addshore
+ * @covers TitleArrayFromResult
+ */
+class TitleArrayFromResultTest extends PHPUnit\Framework\TestCase {
+
+       use MediaWikiCoversValidator;
+
+       private function getMockResultWrapper( $row = null, $numRows = 1 ) {
+               $resultWrapper = $this->getMockBuilder( Wikimedia\Rdbms\ResultWrapper::class )
+                       ->disableOriginalConstructor();
+
+               $resultWrapper = $resultWrapper->getMock();
+               $resultWrapper->expects( $this->atLeastOnce() )
+                       ->method( 'current' )
+                       ->will( $this->returnValue( $row ) );
+               $resultWrapper->expects( $this->any() )
+                       ->method( 'numRows' )
+                       ->will( $this->returnValue( $numRows ) );
+
+               return $resultWrapper;
+       }
+
+       private function getRowWithTitle( $namespace = 3, $title = 'foo' ) {
+               $row = new stdClass();
+               $row->page_namespace = $namespace;
+               $row->page_title = $title;
+               return $row;
+       }
+
+       /**
+        * @covers TitleArrayFromResult::__construct
+        */
+       public function testConstructionWithFalseRow() {
+               $row = false;
+               $resultWrapper = $this->getMockResultWrapper( $row );
+
+               $object = new TitleArrayFromResult( $resultWrapper );
+
+               $this->assertEquals( $resultWrapper, $object->res );
+               $this->assertSame( 0, $object->key );
+               $this->assertEquals( $row, $object->current );
+       }
+
+       /**
+        * @covers TitleArrayFromResult::__construct
+        */
+       public function testConstructionWithRow() {
+               $namespace = 0;
+               $title = 'foo';
+               $row = $this->getRowWithTitle( $namespace, $title );
+               $resultWrapper = $this->getMockResultWrapper( $row );
+
+               $object = new TitleArrayFromResult( $resultWrapper );
+
+               $this->assertEquals( $resultWrapper, $object->res );
+               $this->assertSame( 0, $object->key );
+               $this->assertInstanceOf( Title::class, $object->current );
+               $this->assertEquals( $namespace, $object->current->mNamespace );
+               $this->assertEquals( $title, $object->current->mTextform );
+       }
+
+       public static function provideNumberOfRows() {
+               return [
+                       [ 0 ],
+                       [ 1 ],
+                       [ 122 ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideNumberOfRows
+        * @covers TitleArrayFromResult::count
+        */
+       public function testCountWithVaryingValues( $numRows ) {
+               $object = new TitleArrayFromResult( $this->getMockResultWrapper(
+                       $this->getRowWithTitle(),
+                       $numRows
+               ) );
+               $this->assertEquals( $numRows, $object->count() );
+       }
+
+       /**
+        * @covers TitleArrayFromResult::current
+        */
+       public function testCurrentAfterConstruction() {
+               $namespace = 0;
+               $title = 'foo';
+               $row = $this->getRowWithTitle( $namespace, $title );
+               $object = new TitleArrayFromResult( $this->getMockResultWrapper( $row ) );
+               $this->assertInstanceOf( Title::class, $object->current() );
+               $this->assertEquals( $namespace, $object->current->mNamespace );
+               $this->assertEquals( $title, $object->current->mTextform );
+       }
+
+       public function provideTestValid() {
+               return [
+                       [ $this->getRowWithTitle(), true ],
+                       [ false, false ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideTestValid
+        * @covers TitleArrayFromResult::valid
+        */
+       public function testValid( $input, $expected ) {
+               $object = new TitleArrayFromResult( $this->getMockResultWrapper( $input ) );
+               $this->assertEquals( $expected, $object->valid() );
+       }
+
+       // @todo unit test for key()
+       // @todo unit test for next()
+       // @todo unit test for rewind()
+}
diff --git a/tests/phpunit/unit/includes/WikiReferenceTest.php b/tests/phpunit/unit/includes/WikiReferenceTest.php
new file mode 100644 (file)
index 0000000..e4b21ce
--- /dev/null
@@ -0,0 +1,166 @@
+<?php
+
+/**
+ * @covers WikiReference
+ */
+class WikiReferenceTest extends PHPUnit\Framework\TestCase {
+
+       use MediaWikiCoversValidator;
+
+       public function provideGetDisplayName() {
+               return [
+                       'http' => [ 'foo.bar', 'http://foo.bar' ],
+                       'https' => [ 'foo.bar', 'http://foo.bar' ],
+
+                       // apparently, this is the expected behavior
+                       'invalid' => [ 'purple kittens', 'purple kittens' ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideGetDisplayName
+        */
+       public function testGetDisplayName( $expected, $canonicalServer ) {
+               $reference = new WikiReference( $canonicalServer, '/wiki/$1' );
+               $this->assertEquals( $expected, $reference->getDisplayName() );
+       }
+
+       public function testGetCanonicalServer() {
+               $reference = new WikiReference( 'https://acme.com', '/wiki/$1', '//acme.com' );
+               $this->assertEquals( 'https://acme.com', $reference->getCanonicalServer() );
+       }
+
+       public function provideGetCanonicalUrl() {
+               return [
+                       'no fragment' => [
+                               'https://acme.com/wiki/Foo',
+                               'https://acme.com',
+                               '//acme.com',
+                               '/wiki/$1',
+                               'Foo',
+                               null
+                       ],
+                       'empty fragment' => [
+                               'https://acme.com/wiki/Foo',
+                               'https://acme.com',
+                               '//acme.com',
+                               '/wiki/$1',
+                               'Foo',
+                               ''
+                       ],
+                       'fragment' => [
+                               'https://acme.com/wiki/Foo#Bar',
+                               'https://acme.com',
+                               '//acme.com',
+                               '/wiki/$1',
+                               'Foo',
+                               'Bar'
+                       ],
+                       'double fragment' => [
+                               'https://acme.com/wiki/Foo#Bar%23Xus',
+                               'https://acme.com',
+                               '//acme.com',
+                               '/wiki/$1',
+                               'Foo',
+                               'Bar#Xus'
+                       ],
+                       'escaped fragment' => [
+                               'https://acme.com/wiki/Foo%23Bar',
+                               'https://acme.com',
+                               '//acme.com',
+                               '/wiki/$1',
+                               'Foo#Bar',
+                               null
+                       ],
+                       'empty path' => [
+                               'https://acme.com/Foo',
+                               'https://acme.com',
+                               '//acme.com',
+                               '/$1',
+                               'Foo',
+                               null
+                       ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideGetCanonicalUrl
+        */
+       public function testGetCanonicalUrl(
+               $expected, $canonicalServer, $server, $path, $page, $fragmentId
+       ) {
+               $reference = new WikiReference( $canonicalServer, $path, $server );
+               $this->assertEquals( $expected, $reference->getCanonicalUrl( $page, $fragmentId ) );
+       }
+
+       /**
+        * @dataProvider provideGetCanonicalUrl
+        * @note getUrl is an alias for getCanonicalUrl
+        */
+       public function testGetUrl( $expected, $canonicalServer, $server, $path, $page, $fragmentId ) {
+               $reference = new WikiReference( $canonicalServer, $path, $server );
+               $this->assertEquals( $expected, $reference->getUrl( $page, $fragmentId ) );
+       }
+
+       public function provideGetFullUrl() {
+               return [
+                       'no fragment' => [
+                               '//acme.com/wiki/Foo',
+                               'https://acme.com',
+                               '//acme.com',
+                               '/wiki/$1',
+                               'Foo',
+                               null
+                       ],
+                       'empty fragment' => [
+                               '//acme.com/wiki/Foo',
+                               'https://acme.com',
+                               '//acme.com',
+                               '/wiki/$1',
+                               'Foo',
+                               ''
+                       ],
+                       'fragment' => [
+                               '//acme.com/wiki/Foo#Bar',
+                               'https://acme.com',
+                               '//acme.com',
+                               '/wiki/$1',
+                               'Foo',
+                               'Bar'
+                       ],
+                       'double fragment' => [
+                               '//acme.com/wiki/Foo#Bar%23Xus',
+                               'https://acme.com',
+                               '//acme.com',
+                               '/wiki/$1',
+                               'Foo',
+                               'Bar#Xus'
+                       ],
+                       'escaped fragment' => [
+                               '//acme.com/wiki/Foo%23Bar',
+                               'https://acme.com',
+                               '//acme.com',
+                               '/wiki/$1',
+                               'Foo#Bar',
+                               null
+                       ],
+                       'empty path' => [
+                               '//acme.com/Foo',
+                               'https://acme.com',
+                               '//acme.com',
+                               '/$1',
+                               'Foo',
+                               null
+                       ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideGetFullUrl
+        */
+       public function testGetFullUrl( $expected, $canonicalServer, $server, $path, $page, $fragmentId ) {
+               $reference = new WikiReference( $canonicalServer, $path, $server );
+               $this->assertEquals( $expected, $reference->getFullUrl( $page, $fragmentId ) );
+       }
+
+}
diff --git a/tests/phpunit/unit/includes/XmlJsTest.php b/tests/phpunit/unit/includes/XmlJsTest.php
new file mode 100644 (file)
index 0000000..c7975ef
--- /dev/null
@@ -0,0 +1,26 @@
+<?php
+
+/**
+ * @group Xml
+ */
+class XmlJsTest extends PHPUnit\Framework\TestCase {
+
+       use MediaWikiCoversValidator;
+
+       /**
+        * @covers XmlJsCode::__construct
+        * @dataProvider provideConstruction
+        */
+       public function testConstruction( $value ) {
+               $obj = new XmlJsCode( $value );
+               $this->assertEquals( $value, $obj->value );
+       }
+
+       public static function provideConstruction() {
+               return [
+                       [ null ],
+                       [ '' ],
+               ];
+       }
+
+}
diff --git a/tests/phpunit/unit/includes/XmlSelectTest.php b/tests/phpunit/unit/includes/XmlSelectTest.php
new file mode 100644 (file)
index 0000000..54d269e
--- /dev/null
@@ -0,0 +1,182 @@
+<?php
+
+/**
+ * @group Xml
+ */
+class XmlSelectTest extends \MediaWikiUnitTestCase {
+
+       /**
+        * @var XmlSelect
+        */
+       protected $select;
+
+       protected function setUp() {
+               parent::setUp();
+               $this->select = new XmlSelect();
+       }
+
+       protected function tearDown() {
+               parent::tearDown();
+               $this->select = null;
+       }
+
+       /**
+        * @covers XmlSelect::__construct
+        */
+       public function testConstructWithoutParameters() {
+               $this->assertEquals( '<select></select>', $this->select->getHTML() );
+       }
+
+       /**
+        * Parameters are $name (false), $id (false), $default (false)
+        * @dataProvider provideConstructionParameters
+        * @covers XmlSelect::__construct
+        */
+       public function testConstructParameters( $name, $id, $default, $expected ) {
+               $this->select = new XmlSelect( $name, $id, $default );
+               $this->assertEquals( $expected, $this->select->getHTML() );
+       }
+
+       /**
+        * Provide parameters for testConstructParameters() which use three
+        * parameters:
+        *  - $name    (default: false)
+        *  - $id      (default: false)
+        *  - $default (default: false)
+        * Provides a fourth parameters representing the expected HTML output
+        */
+       public static function provideConstructionParameters() {
+               return [
+                       /**
+                        * Values are set following a 3-bit Gray code where two successive
+                        * values differ by only one value.
+                        * See https://en.wikipedia.org/wiki/Gray_code
+                        */
+                       #      $name   $id    $default
+                       [ false, false, false, '<select></select>' ],
+                       [ false, false, 'foo', '<select></select>' ],
+                       [ false, 'id', 'foo', '<select id="id"></select>' ],
+                       [ false, 'id', false, '<select id="id"></select>' ],
+                       [ 'name', 'id', false, '<select name="name" id="id"></select>' ],
+                       [ 'name', 'id', 'foo', '<select name="name" id="id"></select>' ],
+                       [ 'name', false, 'foo', '<select name="name"></select>' ],
+                       [ 'name', false, false, '<select name="name"></select>' ],
+               ];
+       }
+
+       /**
+        * @covers XmlSelect::addOption
+        */
+       public function testAddOption() {
+               $this->select->addOption( 'foo' );
+               $this->assertEquals(
+                       '<select><option value="foo">foo</option></select>',
+                       $this->select->getHTML()
+               );
+       }
+
+       /**
+        * @covers XmlSelect::addOption
+        */
+       public function testAddOptionWithDefault() {
+               $this->select->addOption( 'foo', true );
+               $this->assertEquals(
+                       '<select><option value="1">foo</option></select>',
+                       $this->select->getHTML()
+               );
+       }
+
+       /**
+        * @covers XmlSelect::addOption
+        */
+       public function testAddOptionWithFalse() {
+               $this->select->addOption( 'foo', false );
+               $this->assertEquals(
+                       '<select><option value="foo">foo</option></select>',
+                       $this->select->getHTML()
+               );
+       }
+
+       /**
+        * @covers XmlSelect::addOption
+        */
+       public function testAddOptionWithValueZero() {
+               $this->select->addOption( 'foo', 0 );
+               $this->assertEquals(
+                       '<select><option value="0">foo</option></select>',
+                       $this->select->getHTML()
+               );
+       }
+
+       /**
+        * @covers XmlSelect::setDefault
+        */
+       public function testSetDefault() {
+               $this->select->setDefault( 'bar1' );
+               $this->select->addOption( 'foo1' );
+               $this->select->addOption( 'bar1' );
+               $this->select->addOption( 'foo2' );
+               $this->assertEquals(
+                       '<select><option value="foo1">foo1</option>' . "\n" .
+                               '<option value="bar1" selected="">bar1</option>' . "\n" .
+                               '<option value="foo2">foo2</option></select>', $this->select->getHTML() );
+       }
+
+       /**
+        * Adding default later on should set the correct selection or
+        * raise an exception.
+        * To handle this, we need to render the options in getHtml()
+        * @covers XmlSelect::setDefault
+        */
+       public function testSetDefaultAfterAddingOptions() {
+               $this->select->addOption( 'foo1' );
+               $this->select->addOption( 'bar1' );
+               $this->select->addOption( 'foo2' );
+               $this->select->setDefault( 'bar1' ); # setting default after adding options
+               $this->assertEquals(
+                       '<select><option value="foo1">foo1</option>' . "\n" .
+                               '<option value="bar1" selected="">bar1</option>' . "\n" .
+                               '<option value="foo2">foo2</option></select>', $this->select->getHTML() );
+       }
+
+       /**
+        * @covers XmlSelect::setAttribute
+        * @covers XmlSelect::getAttribute
+        */
+       public function testGetAttributes() {
+               # create some attributes
+               $this->select->setAttribute( 'dummy', 0x777 );
+               $this->select->setAttribute( 'string', 'euro €' );
+               $this->select->setAttribute( 1911, 'razor' );
+
+               # verify we can retrieve them
+               $this->assertEquals(
+                       $this->select->getAttribute( 'dummy' ),
+                       0x777
+               );
+               $this->assertEquals(
+                       $this->select->getAttribute( 'string' ),
+                       'euro €'
+               );
+               $this->assertEquals(
+                       $this->select->getAttribute( 1911 ),
+                       'razor'
+               );
+
+               # inexistent keys should give us 'null'
+               $this->assertEquals(
+                       $this->select->getAttribute( 'I DO NOT EXIT' ),
+                       null
+               );
+
+               # verify string / integer
+               $this->assertEquals(
+                       $this->select->getAttribute( '1911' ),
+                       'razor'
+               );
+               $this->assertEquals(
+                       $this->select->getAttribute( 'dummy' ),
+                       0x777
+               );
+       }
+}
diff --git a/tests/phpunit/unit/includes/actions/ViewActionTest.php b/tests/phpunit/unit/includes/actions/ViewActionTest.php
new file mode 100644 (file)
index 0000000..99d61b6
--- /dev/null
@@ -0,0 +1,35 @@
+<?php
+
+/**
+ * @covers \ViewAction
+ *
+ * @group Actions
+ *
+ * @author Derick N. Alangi
+ */
+class ViewActionTest extends \MediaWikiUnitTestCase {
+       /**
+        * @return ViewAction
+        */
+       private function makeViewActionClassFactory() {
+               $page = new Article( Title::newMainPage() );
+               $context = RequestContext::getMain();
+               $viewAction = new ViewAction( $page, $context );
+
+               return $viewAction;
+       }
+
+       public function testGetName() {
+               $viewAction = $this->makeViewActionClassFactory();
+               $actual = $viewAction->getName();
+
+               $this->assertSame( 'view', $actual );
+       }
+
+       public function testOnView() {
+               $viewAction = $this->makeViewActionClassFactory();
+               $actual = $viewAction->onView();
+
+               $this->assertNull( $actual );
+       }
+}
diff --git a/tests/phpunit/unit/includes/api/ApiBlockInfoTraitTest.php b/tests/phpunit/unit/includes/api/ApiBlockInfoTraitTest.php
new file mode 100644 (file)
index 0000000..ed5a184
--- /dev/null
@@ -0,0 +1,66 @@
+<?php
+
+use Wikimedia\TestingAccessWrapper;
+use MediaWiki\Block\DatabaseBlock;
+use MediaWiki\Block\SystemBlock;
+
+/**
+ * @covers ApiBlockInfoTrait
+ */
+class ApiBlockInfoTraitTest extends \MediaWikiUnitTestCase {
+
+       protected function setUp() {
+               parent::setUp();
+
+               $lbMock = $this->createMock( LoadBalancer::class );
+               $lbMock->expects( $this->any() )
+                       ->method( 'getConnection' )
+                       ->willReturn( $this->createMock( Database::class ) );
+
+               $loadBalancerMockFactory = function () use ( $lbMock ): LoadBalancer {
+                       return $lbMock;
+               };
+
+               $this->overrideMwServices( [ 'DBLoadBalancer' => $loadBalancerMockFactory ] );
+       }
+
+       /**
+        * @dataProvider provideGetBlockDetails
+        */
+       public function testGetBlockDetails( $blockFactory, $expectedInfo ) {
+               $mock = $this->getMockForTrait( ApiBlockInfoTrait::class );
+               $info = TestingAccessWrapper::newFromObject( $mock )->getBlockDetails( $blockFactory() );
+               $subset = array_merge( [
+                       'blockid' => null,
+                       'blockedby' => '',
+                       'blockedbyid' => 0,
+                       'blockreason' => '',
+                       'blockexpiry' => 'infinite',
+               ], $expectedInfo );
+               $this->assertArraySubset( $subset, $info );
+       }
+
+       public static function provideGetBlockDetails() {
+               return [
+                       'Sitewide block' => [
+                               // Defer instantiation to avoid connecting to DB
+                               function () {
+                                       return new DatabaseBlock();
+                               },
+                               [ 'blockpartial' => false ],
+                       ],
+                       'Partial block' => [
+                               function () {
+                                       return new DatabaseBlock( [ 'sitewide' => false ] );
+                               },
+                               [ 'blockpartial' => true ],
+                       ],
+                       'System block' => [
+                               function () {
+                                       return new SystemBlock( [ 'systemBlock' => 'proxy' ] );
+                               },
+                               [ 'systemblocktype' => 'proxy' ]
+                       ],
+               ];
+       }
+}
diff --git a/tests/phpunit/unit/includes/api/ApiContinuationManagerTest.php b/tests/phpunit/unit/includes/api/ApiContinuationManagerTest.php
new file mode 100644 (file)
index 0000000..cc1351b
--- /dev/null
@@ -0,0 +1,198 @@
+<?php
+
+/**
+ * @covers ApiContinuationManager
+ * @group API
+ */
+class ApiContinuationManagerTest extends \MediaWikiUnitTestCase {
+
+       private static function getManager( $continue, $allModules, $generatedModules ) {
+               $context = new DerivativeContext( RequestContext::getMain() );
+               $context->setRequest( new FauxRequest( [ 'continue' => $continue ] ) );
+               $main = new ApiMain( $context );
+               return new ApiContinuationManager( $main, $allModules, $generatedModules );
+       }
+
+       public function testContinuation() {
+               $allModules = [
+                       new MockApiQueryBase( 'mock1' ),
+                       new MockApiQueryBase( 'mock2' ),
+                       new MockApiQueryBase( 'mocklist' ),
+               ];
+               $generator = new MockApiQueryBase( 'generator' );
+
+               $manager = self::getManager( '', $allModules, [ 'mock1', 'mock2' ] );
+               $this->assertSame( ApiMain::class, $manager->getSource() );
+               $this->assertSame( false, $manager->isGeneratorDone() );
+               $this->assertSame( $allModules, $manager->getRunModules() );
+               $manager->addContinueParam( $allModules[0], 'm1continue', [ 1, 2 ] );
+               $manager->addContinueParam( $allModules[2], 'mlcontinue', 2 );
+               $manager->addGeneratorContinueParam( $generator, 'gcontinue', 3 );
+               $this->assertSame( [ [
+                       'mlcontinue' => 2,
+                       'm1continue' => '1|2',
+                       'continue' => '||mock2',
+               ], false ], $manager->getContinuation() );
+               $this->assertSame( [
+                       'mock1' => [ 'm1continue' => '1|2' ],
+                       'mocklist' => [ 'mlcontinue' => 2 ],
+                       'generator' => [ 'gcontinue' => 3 ],
+               ], $manager->getRawContinuation() );
+
+               $result = new ApiResult( 0 );
+               $manager->setContinuationIntoResult( $result );
+               $this->assertSame( [
+                       'mlcontinue' => 2,
+                       'm1continue' => '1|2',
+                       'continue' => '||mock2',
+               ], $result->getResultData( 'continue' ) );
+               $this->assertSame( null, $result->getResultData( 'batchcomplete' ) );
+
+               $manager = self::getManager( '', $allModules, [ 'mock1', 'mock2' ] );
+               $this->assertSame( false, $manager->isGeneratorDone() );
+               $this->assertSame( $allModules, $manager->getRunModules() );
+               $manager->addContinueParam( $allModules[0], 'm1continue', [ 1, 2 ] );
+               $manager->addGeneratorContinueParam( $generator, 'gcontinue', [ 3, 4 ] );
+               $this->assertSame( [ [
+                       'm1continue' => '1|2',
+                       'continue' => '||mock2|mocklist',
+               ], false ], $manager->getContinuation() );
+               $this->assertSame( [
+                       'mock1' => [ 'm1continue' => '1|2' ],
+                       'generator' => [ 'gcontinue' => '3|4' ],
+               ], $manager->getRawContinuation() );
+
+               $manager = self::getManager( '', $allModules, [ 'mock1', 'mock2' ] );
+               $this->assertSame( false, $manager->isGeneratorDone() );
+               $this->assertSame( $allModules, $manager->getRunModules() );
+               $manager->addContinueParam( $allModules[2], 'mlcontinue', 2 );
+               $manager->addGeneratorContinueParam( $generator, 'gcontinue', 3 );
+               $this->assertSame( [ [
+                       'mlcontinue' => 2,
+                       'gcontinue' => 3,
+                       'continue' => 'gcontinue||',
+               ], true ], $manager->getContinuation() );
+               $this->assertSame( [
+                       'mocklist' => [ 'mlcontinue' => 2 ],
+                       'generator' => [ 'gcontinue' => 3 ],
+               ], $manager->getRawContinuation() );
+
+               $result = new ApiResult( 0 );
+               $manager->setContinuationIntoResult( $result );
+               $this->assertSame( [
+                       'mlcontinue' => 2,
+                       'gcontinue' => 3,
+                       'continue' => 'gcontinue||',
+               ], $result->getResultData( 'continue' ) );
+               $this->assertSame( true, $result->getResultData( 'batchcomplete' ) );
+
+               $manager = self::getManager( '', $allModules, [ 'mock1', 'mock2' ] );
+               $this->assertSame( false, $manager->isGeneratorDone() );
+               $this->assertSame( $allModules, $manager->getRunModules() );
+               $manager->addGeneratorContinueParam( $generator, 'gcontinue', 3 );
+               $this->assertSame( [ [
+                       'gcontinue' => 3,
+                       'continue' => 'gcontinue||mocklist',
+               ], true ], $manager->getContinuation() );
+               $this->assertSame( [
+                       'generator' => [ 'gcontinue' => 3 ],
+               ], $manager->getRawContinuation() );
+
+               $manager = self::getManager( '', $allModules, [ 'mock1', 'mock2' ] );
+               $this->assertSame( false, $manager->isGeneratorDone() );
+               $this->assertSame( $allModules, $manager->getRunModules() );
+               $manager->addContinueParam( $allModules[0], 'm1continue', [ 1, 2 ] );
+               $manager->addContinueParam( $allModules[2], 'mlcontinue', 2 );
+               $this->assertSame( [ [
+                       'mlcontinue' => 2,
+                       'm1continue' => '1|2',
+                       'continue' => '||mock2',
+               ], false ], $manager->getContinuation() );
+               $this->assertSame( [
+                       'mock1' => [ 'm1continue' => '1|2' ],
+                       'mocklist' => [ 'mlcontinue' => 2 ],
+               ], $manager->getRawContinuation() );
+
+               $manager = self::getManager( '', $allModules, [ 'mock1', 'mock2' ] );
+               $this->assertSame( false, $manager->isGeneratorDone() );
+               $this->assertSame( $allModules, $manager->getRunModules() );
+               $manager->addContinueParam( $allModules[0], 'm1continue', [ 1, 2 ] );
+               $this->assertSame( [ [
+                       'm1continue' => '1|2',
+                       'continue' => '||mock2|mocklist',
+               ], false ], $manager->getContinuation() );
+               $this->assertSame( [
+                       'mock1' => [ 'm1continue' => '1|2' ],
+               ], $manager->getRawContinuation() );
+
+               $manager = self::getManager( '', $allModules, [ 'mock1', 'mock2' ] );
+               $this->assertSame( false, $manager->isGeneratorDone() );
+               $this->assertSame( $allModules, $manager->getRunModules() );
+               $manager->addContinueParam( $allModules[2], 'mlcontinue', 2 );
+               $this->assertSame( [ [
+                       'mlcontinue' => 2,
+                       'continue' => '-||mock1|mock2',
+               ], true ], $manager->getContinuation() );
+               $this->assertSame( [
+                       'mocklist' => [ 'mlcontinue' => 2 ],
+               ], $manager->getRawContinuation() );
+
+               $manager = self::getManager( '', $allModules, [ 'mock1', 'mock2' ] );
+               $this->assertSame( false, $manager->isGeneratorDone() );
+               $this->assertSame( $allModules, $manager->getRunModules() );
+               $this->assertSame( [ [], true ], $manager->getContinuation() );
+               $this->assertSame( [], $manager->getRawContinuation() );
+
+               $manager = self::getManager( '||mock2', $allModules, [ 'mock1', 'mock2' ] );
+               $this->assertSame( false, $manager->isGeneratorDone() );
+               $this->assertSame(
+                       array_values( array_diff_key( $allModules, [ 1 => 1 ] ) ),
+                       $manager->getRunModules()
+               );
+
+               $manager = self::getManager( '-||', $allModules, [ 'mock1', 'mock2' ] );
+               $this->assertSame( true, $manager->isGeneratorDone() );
+               $this->assertSame(
+                       array_values( array_diff_key( $allModules, [ 0 => 0, 1 => 1 ] ) ),
+                       $manager->getRunModules()
+               );
+
+               try {
+                       self::getManager( 'foo', $allModules, [ 'mock1', 'mock2' ] );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( ApiUsageException $ex ) {
+                       $this->assertTrue( ApiTestCase::apiExceptionHasCode( $ex, 'badcontinue' ),
+                               'Expected exception'
+                       );
+               }
+
+               $manager = self::getManager(
+                       '||mock2',
+                       array_slice( $allModules, 0, 2 ),
+                       [ 'mock1', 'mock2' ]
+               );
+               try {
+                       $manager->addContinueParam( $allModules[1], 'm2continue', 1 );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( UnexpectedValueException $ex ) {
+                       $this->assertSame(
+                               'Module \'mock2\' was not supposed to have been executed, ' .
+                                       'but it was executed anyway',
+                               $ex->getMessage(),
+                               'Expected exception'
+                       );
+               }
+               try {
+                       $manager->addContinueParam( $allModules[2], 'mlcontinue', 1 );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( UnexpectedValueException $ex ) {
+                       $this->assertSame(
+                               'Module \'mocklist\' called ApiContinuationManager::addContinueParam ' .
+                                       'but was not passed to ApiContinuationManager::__construct',
+                               $ex->getMessage(),
+                               'Expected exception'
+                       );
+               }
+       }
+
+}
diff --git a/tests/phpunit/unit/includes/api/ApiMessageTest.php b/tests/phpunit/unit/includes/api/ApiMessageTest.php
new file mode 100644 (file)
index 0000000..d6fa780
--- /dev/null
@@ -0,0 +1,196 @@
+<?php
+
+use Wikimedia\TestingAccessWrapper;
+
+/**
+ * @group API
+ */
+class ApiMessageTest extends \MediaWikiUnitTestCase {
+
+       private function compareMessages( Message $msg, Message $msg2 ) {
+               $this->assertSame( $msg->getKey(), $msg2->getKey(), 'getKey' );
+               $this->assertSame( $msg->getKeysToTry(), $msg2->getKeysToTry(), 'getKeysToTry' );
+               $this->assertSame( $msg->getParams(), $msg2->getParams(), 'getParams' );
+               $this->assertSame( $msg->getLanguage(), $msg2->getLanguage(), 'getLanguage' );
+
+               $msg = TestingAccessWrapper::newFromObject( $msg );
+               $msg2 = TestingAccessWrapper::newFromObject( $msg2 );
+               $this->assertSame( $msg->interface, $msg2->interface, 'interface' );
+               $this->assertSame( $msg->useDatabase, $msg2->useDatabase, 'useDatabase' );
+               $this->assertSame( $msg->format, $msg2->format, 'format' );
+               $this->assertSame(
+                       $msg->title ? $msg->title->getFullText() : null,
+                       $msg2->title ? $msg2->title->getFullText() : null,
+                       'title'
+               );
+       }
+
+       /**
+        * @covers ApiMessageTrait
+        */
+       public function testCodeDefaults() {
+               $msg = new ApiMessage( 'foo' );
+               $this->assertSame( 'foo', $msg->getApiCode() );
+
+               $msg = new ApiMessage( 'apierror-bar' );
+               $this->assertSame( 'bar', $msg->getApiCode() );
+
+               $msg = new ApiMessage( 'apiwarn-baz' );
+               $this->assertSame( 'baz', $msg->getApiCode() );
+
+               // Weird "message key"
+               $msg = new ApiMessage( "<foo> bar\nbaz" );
+               $this->assertSame( '_foo__bar_baz', $msg->getApiCode() );
+
+               // BC case
+               $msg = new ApiMessage( 'actionthrottledtext' );
+               $this->assertSame( 'ratelimited', $msg->getApiCode() );
+
+               $msg = new ApiMessage( [ 'apierror-missingparam', 'param' ] );
+               $this->assertSame( 'noparam', $msg->getApiCode() );
+       }
+
+       /**
+        * @covers ApiMessageTrait
+        * @dataProvider provideInvalidCode
+        * @param mixed $code
+        */
+       public function testInvalidCode( $code ) {
+               $msg = new ApiMessage( 'foo' );
+               try {
+                       $msg->setApiCode( $code );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( InvalidArgumentException $ex ) {
+                       $this->assertTrue( true );
+               }
+
+               try {
+                       new ApiMessage( 'foo', $code );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( InvalidArgumentException $ex ) {
+                       $this->assertTrue( true );
+               }
+       }
+
+       public static function provideInvalidCode() {
+               return [
+                       [ '' ],
+                       [ 42 ],
+                       [ 'A bad code' ],
+                       [ 'Project:A_page_title' ],
+                       [ "WTF\nnewlines" ],
+               ];
+       }
+
+       /**
+        * @covers ApiMessage
+        * @covers ApiMessageTrait
+        */
+       public function testApiMessage() {
+               $msg = new Message( [ 'foo', 'bar' ], [ 'baz' ] );
+               $msg->inLanguage( 'de' )->title( Title::newMainPage() );
+               $msg2 = new ApiMessage( $msg, 'code', [ 'data' ] );
+               $this->compareMessages( $msg, $msg2 );
+               $this->assertEquals( 'code', $msg2->getApiCode() );
+               $this->assertEquals( [ 'data' ], $msg2->getApiData() );
+
+               $msg2 = unserialize( serialize( $msg2 ) );
+               $this->compareMessages( $msg, $msg2 );
+               $this->assertEquals( 'code', $msg2->getApiCode() );
+               $this->assertEquals( [ 'data' ], $msg2->getApiData() );
+
+               $msg = new Message( [ 'foo', 'bar' ], [ 'baz' ] );
+               $msg2 = new ApiMessage( [ [ 'foo', 'bar' ], 'baz' ], 'code', [ 'data' ] );
+               $this->compareMessages( $msg, $msg2 );
+               $this->assertEquals( 'code', $msg2->getApiCode() );
+               $this->assertEquals( [ 'data' ], $msg2->getApiData() );
+
+               $msg = new Message( 'foo' );
+               $msg2 = new ApiMessage( 'foo' );
+               $this->compareMessages( $msg, $msg2 );
+               $this->assertEquals( 'foo', $msg2->getApiCode() );
+               $this->assertEquals( [], $msg2->getApiData() );
+
+               $msg2->setApiCode( 'code', [ 'data' ] );
+               $this->assertEquals( 'code', $msg2->getApiCode() );
+               $this->assertEquals( [ 'data' ], $msg2->getApiData() );
+               $msg2->setApiCode( null );
+               $this->assertEquals( 'foo', $msg2->getApiCode() );
+               $this->assertEquals( [ 'data' ], $msg2->getApiData() );
+               $msg2->setApiData( [ 'data2' ] );
+               $this->assertEquals( [ 'data2' ], $msg2->getApiData() );
+       }
+
+       /**
+        * @covers ApiRawMessage
+        * @covers ApiMessageTrait
+        */
+       public function testApiRawMessage() {
+               $msg = new RawMessage( 'foo', [ 'baz' ] );
+               $msg->inLanguage( 'de' )->title( Title::newMainPage() );
+               $msg2 = new ApiRawMessage( $msg, 'code', [ 'data' ] );
+               $this->compareMessages( $msg, $msg2 );
+               $this->assertEquals( 'code', $msg2->getApiCode() );
+               $this->assertEquals( [ 'data' ], $msg2->getApiData() );
+
+               $msg2 = unserialize( serialize( $msg2 ) );
+               $this->compareMessages( $msg, $msg2 );
+               $this->assertEquals( 'code', $msg2->getApiCode() );
+               $this->assertEquals( [ 'data' ], $msg2->getApiData() );
+
+               $msg = new RawMessage( 'foo', [ 'baz' ] );
+               $msg2 = new ApiRawMessage( [ 'foo', 'baz' ], 'code', [ 'data' ] );
+               $this->compareMessages( $msg, $msg2 );
+               $this->assertEquals( 'code', $msg2->getApiCode() );
+               $this->assertEquals( [ 'data' ], $msg2->getApiData() );
+
+               $msg = new RawMessage( 'foo' );
+               $msg2 = new ApiRawMessage( 'foo', 'code', [ 'data' ] );
+               $this->compareMessages( $msg, $msg2 );
+               $this->assertEquals( 'code', $msg2->getApiCode() );
+               $this->assertEquals( [ 'data' ], $msg2->getApiData() );
+
+               $msg2->setApiCode( 'code', [ 'data' ] );
+               $this->assertEquals( 'code', $msg2->getApiCode() );
+               $this->assertEquals( [ 'data' ], $msg2->getApiData() );
+               $msg2->setApiCode( null );
+               $this->assertEquals( 'foo', $msg2->getApiCode() );
+               $this->assertEquals( [ 'data' ], $msg2->getApiData() );
+               $msg2->setApiData( [ 'data2' ] );
+               $this->assertEquals( [ 'data2' ], $msg2->getApiData() );
+       }
+
+       /**
+        * @covers ApiMessage::create
+        */
+       public function testApiMessageCreate() {
+               $this->assertInstanceOf( ApiMessage::class, ApiMessage::create( new Message( 'mainpage' ) ) );
+               $this->assertInstanceOf(
+                       ApiRawMessage::class, ApiMessage::create( new RawMessage( 'mainpage' ) )
+               );
+               $this->assertInstanceOf( ApiMessage::class, ApiMessage::create( 'mainpage' ) );
+
+               $msg = new ApiMessage( [ 'parentheses', 'foobar' ] );
+               $msg2 = new Message( 'parentheses', [ 'foobar' ] );
+
+               $this->assertSame( $msg, ApiMessage::create( $msg ) );
+               $this->assertEquals( $msg, ApiMessage::create( $msg2 ) );
+               $this->assertEquals( $msg, ApiMessage::create( [ 'parentheses', 'foobar' ] ) );
+               $this->assertEquals( $msg,
+                       ApiMessage::create( [ 'message' => 'parentheses', 'params' => [ 'foobar' ] ] )
+               );
+               $this->assertSame( $msg,
+                       ApiMessage::create( [ 'message' => $msg, 'params' => [ 'xxx' ] ] )
+               );
+               $this->assertEquals( $msg,
+                       ApiMessage::create( [ 'message' => $msg2, 'params' => [ 'xxx' ] ] )
+               );
+               $this->assertSame( $msg,
+                       ApiMessage::create( [ 'message' => $msg ] )
+               );
+
+               $msg = new ApiRawMessage( [ 'parentheses', 'foobar' ] );
+               $this->assertSame( $msg, ApiMessage::create( $msg ) );
+       }
+
+}
diff --git a/tests/phpunit/unit/includes/api/ApiResultTest.php b/tests/phpunit/unit/includes/api/ApiResultTest.php
new file mode 100644 (file)
index 0000000..2d99890
--- /dev/null
@@ -0,0 +1,1410 @@
+<?php
+
+/**
+ * @covers ApiResult
+ * @group API
+ */
+class ApiResultTest extends \MediaWikiUnitTestCase {
+
+       /**
+        * @covers ApiResult
+        */
+       public function testStaticDataMethods() {
+               $arr = [];
+
+               ApiResult::setValue( $arr, 'setValue', '1' );
+
+               ApiResult::setValue( $arr, null, 'unnamed 1' );
+               ApiResult::setValue( $arr, null, 'unnamed 2' );
+
+               ApiResult::setValue( $arr, 'deleteValue', '2' );
+               ApiResult::unsetValue( $arr, 'deleteValue' );
+
+               ApiResult::setContentValue( $arr, 'setContentValue', '3' );
+
+               $this->assertSame( [
+                       'setValue' => '1',
+                       'unnamed 1',
+                       'unnamed 2',
+                       ApiResult::META_CONTENT => 'setContentValue',
+                       'setContentValue' => '3',
+               ], $arr );
+
+               try {
+                       ApiResult::setValue( $arr, 'setValue', '99' );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( RuntimeException $ex ) {
+                       $this->assertSame(
+                               'Attempting to add element setValue=99, existing value is 1',
+                               $ex->getMessage(),
+                               'Expected exception'
+                       );
+               }
+
+               try {
+                       ApiResult::setContentValue( $arr, 'setContentValue2', '99' );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( RuntimeException $ex ) {
+                       $this->assertSame(
+                               'Attempting to set content element as setContentValue2 when setContentValue ' .
+                                       'is already set as the content element',
+                               $ex->getMessage(),
+                               'Expected exception'
+                       );
+               }
+
+               ApiResult::setValue( $arr, 'setValue', '99', ApiResult::OVERRIDE );
+               $this->assertSame( '99', $arr['setValue'] );
+
+               ApiResult::setContentValue( $arr, 'setContentValue2', '99', ApiResult::OVERRIDE );
+               $this->assertSame( 'setContentValue2', $arr[ApiResult::META_CONTENT] );
+
+               $arr = [ 'foo' => 1, 'bar' => 1 ];
+               ApiResult::setValue( $arr, 'top', '2', ApiResult::ADD_ON_TOP );
+               ApiResult::setValue( $arr, null, '2', ApiResult::ADD_ON_TOP );
+               ApiResult::setValue( $arr, 'bottom', '2' );
+               ApiResult::setValue( $arr, 'foo', '2', ApiResult::OVERRIDE );
+               ApiResult::setValue( $arr, 'bar', '2', ApiResult::OVERRIDE | ApiResult::ADD_ON_TOP );
+               $this->assertSame( [ 0, 'top', 'foo', 'bar', 'bottom' ], array_keys( $arr ) );
+
+               $arr = [];
+               ApiResult::setValue( $arr, 'sub', [ 'foo' => 1 ] );
+               ApiResult::setValue( $arr, 'sub', [ 'bar' => 1 ] );
+               $this->assertSame( [ 'sub' => [ 'foo' => 1, 'bar' => 1 ] ], $arr );
+
+               try {
+                       ApiResult::setValue( $arr, 'sub', [ 'foo' => 2, 'baz' => 2 ] );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( RuntimeException $ex ) {
+                       $this->assertSame(
+                               'Conflicting keys (foo) when attempting to merge element sub',
+                               $ex->getMessage(),
+                               'Expected exception'
+                       );
+               }
+
+               $arr = [];
+               $title = Title::newFromText( "MediaWiki:Foobar" );
+               $obj = new stdClass;
+               $obj->foo = 1;
+               $obj->bar = 2;
+               ApiResult::setValue( $arr, 'title', $title );
+               ApiResult::setValue( $arr, 'obj', $obj );
+               $this->assertSame( [
+                       'title' => (string)$title,
+                       'obj' => [ 'foo' => 1, 'bar' => 2, ApiResult::META_TYPE => 'assoc' ],
+               ], $arr );
+
+               $fh = tmpfile();
+               try {
+                       ApiResult::setValue( $arr, 'file', $fh );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( InvalidArgumentException $ex ) {
+                       $this->assertSame(
+                               'Cannot add resource(stream) to ApiResult',
+                               $ex->getMessage(),
+                               'Expected exception'
+                       );
+               }
+               try {
+                       ApiResult::setValue( $arr, null, $fh );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( InvalidArgumentException $ex ) {
+                       $this->assertSame(
+                               'Cannot add resource(stream) to ApiResult',
+                               $ex->getMessage(),
+                               'Expected exception'
+                       );
+               }
+               try {
+                       $obj->file = $fh;
+                       ApiResult::setValue( $arr, 'sub', $obj );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( InvalidArgumentException $ex ) {
+                       $this->assertSame(
+                               'Cannot add resource(stream) to ApiResult',
+                               $ex->getMessage(),
+                               'Expected exception'
+                       );
+               }
+               try {
+                       $obj->file = $fh;
+                       ApiResult::setValue( $arr, null, $obj );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( InvalidArgumentException $ex ) {
+                       $this->assertSame(
+                               'Cannot add resource(stream) to ApiResult',
+                               $ex->getMessage(),
+                               'Expected exception'
+                       );
+               }
+               fclose( $fh );
+
+               try {
+                       ApiResult::setValue( $arr, 'inf', INF );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( InvalidArgumentException $ex ) {
+                       $this->assertSame(
+                               'Cannot add non-finite floats to ApiResult',
+                               $ex->getMessage(),
+                               'Expected exception'
+                       );
+               }
+               try {
+                       ApiResult::setValue( $arr, null, INF );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( InvalidArgumentException $ex ) {
+                       $this->assertSame(
+                               'Cannot add non-finite floats to ApiResult',
+                               $ex->getMessage(),
+                               'Expected exception'
+                       );
+               }
+               try {
+                       ApiResult::setValue( $arr, 'nan', NAN );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( InvalidArgumentException $ex ) {
+                       $this->assertSame(
+                               'Cannot add non-finite floats to ApiResult',
+                               $ex->getMessage(),
+                               'Expected exception'
+                       );
+               }
+               try {
+                       ApiResult::setValue( $arr, null, NAN );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( InvalidArgumentException $ex ) {
+                       $this->assertSame(
+                               'Cannot add non-finite floats to ApiResult',
+                               $ex->getMessage(),
+                               'Expected exception'
+                       );
+               }
+
+               ApiResult::setValue( $arr, null, NAN, ApiResult::NO_VALIDATE );
+
+               try {
+                       ApiResult::setValue( $arr, null, NAN, ApiResult::NO_SIZE_CHECK );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( InvalidArgumentException $ex ) {
+                       $this->assertSame(
+                               'Cannot add non-finite floats to ApiResult',
+                               $ex->getMessage(),
+                               'Expected exception'
+                       );
+               }
+
+               $arr = [];
+               $result2 = new ApiResult( 8388608 );
+               $result2->addValue( null, 'foo', 'bar' );
+               ApiResult::setValue( $arr, 'baz', $result2 );
+               $this->assertSame( [
+                       'baz' => [
+                               ApiResult::META_TYPE => 'assoc',
+                               'foo' => 'bar',
+                       ]
+               ], $arr );
+
+               $arr = [];
+               ApiResult::setValue( $arr, 'foo', "foo\x80bar" );
+               ApiResult::setValue( $arr, 'bar', "a\xcc\x81" );
+               ApiResult::setValue( $arr, 'baz', 74 );
+               ApiResult::setValue( $arr, null, "foo\x80bar" );
+               ApiResult::setValue( $arr, null, "a\xcc\x81" );
+               $this->assertSame( [
+                       'foo' => "foo\xef\xbf\xbdbar",
+                       'bar' => "\xc3\xa1",
+                       'baz' => 74,
+                       0 => "foo\xef\xbf\xbdbar",
+                       1 => "\xc3\xa1",
+               ], $arr );
+
+               $obj = new stdClass;
+               $obj->{'1'} = 'one';
+               $arr = [];
+               ApiResult::setValue( $arr, 'foo', $obj );
+               $this->assertSame( [
+                       'foo' => [
+                               1 => 'one',
+                               ApiResult::META_TYPE => 'assoc',
+                       ]
+               ], $arr );
+       }
+
+       /**
+        * @covers ApiResult
+        */
+       public function testInstanceDataMethods() {
+               $result = new ApiResult( 8388608 );
+
+               $result->addValue( null, 'setValue', '1' );
+
+               $result->addValue( null, null, 'unnamed 1' );
+               $result->addValue( null, null, 'unnamed 2' );
+
+               $result->addValue( null, 'deleteValue', '2' );
+               $result->removeValue( null, 'deleteValue' );
+
+               $result->addValue( [ 'a', 'b' ], 'deleteValue', '3' );
+               $result->removeValue( [ 'a', 'b', 'deleteValue' ], null, '3' );
+
+               $result->addContentValue( null, 'setContentValue', '3' );
+
+               $this->assertSame( [
+                       'setValue' => '1',
+                       'unnamed 1',
+                       'unnamed 2',
+                       'a' => [ 'b' => [] ],
+                       'setContentValue' => '3',
+                       ApiResult::META_TYPE => 'assoc',
+                       ApiResult::META_CONTENT => 'setContentValue',
+               ], $result->getResultData() );
+               $this->assertSame( 20, $result->getSize() );
+
+               try {
+                       $result->addValue( null, 'setValue', '99' );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( RuntimeException $ex ) {
+                       $this->assertSame(
+                               'Attempting to add element setValue=99, existing value is 1',
+                               $ex->getMessage(),
+                               'Expected exception'
+                       );
+               }
+
+               try {
+                       $result->addContentValue( null, 'setContentValue2', '99' );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( RuntimeException $ex ) {
+                       $this->assertSame(
+                               'Attempting to set content element as setContentValue2 when setContentValue ' .
+                                       'is already set as the content element',
+                               $ex->getMessage(),
+                               'Expected exception'
+                       );
+               }
+
+               $result->addValue( null, 'setValue', '99', ApiResult::OVERRIDE );
+               $this->assertSame( '99', $result->getResultData( [ 'setValue' ] ) );
+
+               $result->addContentValue( null, 'setContentValue2', '99', ApiResult::OVERRIDE );
+               $this->assertSame( 'setContentValue2',
+                       $result->getResultData( [ ApiResult::META_CONTENT ] ) );
+
+               $result->reset();
+               $this->assertSame( [
+                       ApiResult::META_TYPE => 'assoc',
+               ], $result->getResultData() );
+               $this->assertSame( 0, $result->getSize() );
+
+               $result->addValue( null, 'foo', 1 );
+               $result->addValue( null, 'bar', 1 );
+               $result->addValue( null, 'top', '2', ApiResult::ADD_ON_TOP );
+               $result->addValue( null, null, '2', ApiResult::ADD_ON_TOP );
+               $result->addValue( null, 'bottom', '2' );
+               $result->addValue( null, 'foo', '2', ApiResult::OVERRIDE );
+               $result->addValue( null, 'bar', '2', ApiResult::OVERRIDE | ApiResult::ADD_ON_TOP );
+               $this->assertSame( [ 0, 'top', 'foo', 'bar', 'bottom', ApiResult::META_TYPE ],
+                       array_keys( $result->getResultData() ) );
+
+               $result->reset();
+               $result->addValue( null, 'foo', [ 'bar' => 1 ] );
+               $result->addValue( [ 'foo', 'top' ], 'x', 2, ApiResult::ADD_ON_TOP );
+               $result->addValue( [ 'foo', 'bottom' ], 'x', 2 );
+               $this->assertSame( [ 'top', 'bar', 'bottom' ],
+                       array_keys( $result->getResultData( [ 'foo' ] ) ) );
+
+               $result->reset();
+               $result->addValue( null, 'sub', [ 'foo' => 1 ] );
+               $result->addValue( null, 'sub', [ 'bar' => 1 ] );
+               $this->assertSame( [
+                       'sub' => [ 'foo' => 1, 'bar' => 1 ],
+                       ApiResult::META_TYPE => 'assoc',
+               ], $result->getResultData() );
+
+               try {
+                       $result->addValue( null, 'sub', [ 'foo' => 2, 'baz' => 2 ] );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( RuntimeException $ex ) {
+                       $this->assertSame(
+                               'Conflicting keys (foo) when attempting to merge element sub',
+                               $ex->getMessage(),
+                               'Expected exception'
+                       );
+               }
+
+               $result->reset();
+               $title = Title::newFromText( "MediaWiki:Foobar" );
+               $obj = new stdClass;
+               $obj->foo = 1;
+               $obj->bar = 2;
+               $result->addValue( null, 'title', $title );
+               $result->addValue( null, 'obj', $obj );
+               $this->assertSame( [
+                       'title' => (string)$title,
+                       'obj' => [ 'foo' => 1, 'bar' => 2, ApiResult::META_TYPE => 'assoc' ],
+                       ApiResult::META_TYPE => 'assoc',
+               ], $result->getResultData() );
+
+               $fh = tmpfile();
+               try {
+                       $result->addValue( null, 'file', $fh );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( InvalidArgumentException $ex ) {
+                       $this->assertSame(
+                               'Cannot add resource(stream) to ApiResult',
+                               $ex->getMessage(),
+                               'Expected exception'
+                       );
+               }
+               try {
+                       $result->addValue( null, null, $fh );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( InvalidArgumentException $ex ) {
+                       $this->assertSame(
+                               'Cannot add resource(stream) to ApiResult',
+                               $ex->getMessage(),
+                               'Expected exception'
+                       );
+               }
+               try {
+                       $obj->file = $fh;
+                       $result->addValue( null, 'sub', $obj );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( InvalidArgumentException $ex ) {
+                       $this->assertSame(
+                               'Cannot add resource(stream) to ApiResult',
+                               $ex->getMessage(),
+                               'Expected exception'
+                       );
+               }
+               try {
+                       $obj->file = $fh;
+                       $result->addValue( null, null, $obj );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( InvalidArgumentException $ex ) {
+                       $this->assertSame(
+                               'Cannot add resource(stream) to ApiResult',
+                               $ex->getMessage(),
+                               'Expected exception'
+                       );
+               }
+               fclose( $fh );
+
+               try {
+                       $result->addValue( null, 'inf', INF );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( InvalidArgumentException $ex ) {
+                       $this->assertSame(
+                               'Cannot add non-finite floats to ApiResult',
+                               $ex->getMessage(),
+                               'Expected exception'
+                       );
+               }
+               try {
+                       $result->addValue( null, null, INF );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( InvalidArgumentException $ex ) {
+                       $this->assertSame(
+                               'Cannot add non-finite floats to ApiResult',
+                               $ex->getMessage(),
+                               'Expected exception'
+                       );
+               }
+               try {
+                       $result->addValue( null, 'nan', NAN );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( InvalidArgumentException $ex ) {
+                       $this->assertSame(
+                               'Cannot add non-finite floats to ApiResult',
+                               $ex->getMessage(),
+                               'Expected exception'
+                       );
+               }
+               try {
+                       $result->addValue( null, null, NAN );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( InvalidArgumentException $ex ) {
+                       $this->assertSame(
+                               'Cannot add non-finite floats to ApiResult',
+                               $ex->getMessage(),
+                               'Expected exception'
+                       );
+               }
+
+               $result->addValue( null, null, NAN, ApiResult::NO_VALIDATE );
+
+               try {
+                       $result->addValue( null, null, NAN, ApiResult::NO_SIZE_CHECK );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( InvalidArgumentException $ex ) {
+                       $this->assertSame(
+                               'Cannot add non-finite floats to ApiResult',
+                               $ex->getMessage(),
+                               'Expected exception'
+                       );
+               }
+
+               $result->reset();
+               $result->addParsedLimit( 'foo', 12 );
+               $this->assertSame( [
+                       'limits' => [ 'foo' => 12 ],
+                       ApiResult::META_TYPE => 'assoc',
+               ], $result->getResultData() );
+               $result->addParsedLimit( 'foo', 13 );
+               $this->assertSame( [
+                       'limits' => [ 'foo' => 13 ],
+                       ApiResult::META_TYPE => 'assoc',
+               ], $result->getResultData() );
+               $this->assertSame( null, $result->getResultData( [ 'foo', 'bar', 'baz' ] ) );
+               $this->assertSame( 13, $result->getResultData( [ 'limits', 'foo' ] ) );
+               try {
+                       $result->getResultData( [ 'limits', 'foo', 'bar' ] );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( InvalidArgumentException $ex ) {
+                       $this->assertSame(
+                               'Path limits.foo is not an array',
+                               $ex->getMessage(),
+                               'Expected exception'
+                       );
+               }
+
+               // Add two values and some metadata, but ensure metadata is not counted
+               $result = new ApiResult( 100 );
+               $obj = [ 'attr' => '12345' ];
+               ApiResult::setContentValue( $obj, 'content', '1234567890' );
+               $this->assertTrue( $result->addValue( null, 'foo', $obj ) );
+               $this->assertSame( 15, $result->getSize() );
+
+               $result = new ApiResult( 10 );
+               $formatter = new ApiErrorFormatter( $result, Language::factory( 'en' ), 'none', false );
+               $result->setErrorFormatter( $formatter );
+               $this->assertFalse( $result->addValue( null, 'foo', '12345678901' ) );
+               $this->assertTrue( $result->addValue( null, 'foo', '12345678901', ApiResult::NO_SIZE_CHECK ) );
+               $this->assertSame( 0, $result->getSize() );
+               $result->reset();
+               $this->assertTrue( $result->addValue( null, 'foo', '1234567890' ) );
+               $this->assertFalse( $result->addValue( null, 'foo', '1' ) );
+               $result->removeValue( null, 'foo' );
+               $this->assertTrue( $result->addValue( null, 'foo', '1' ) );
+
+               $result = new ApiResult( 10 );
+               $obj = new ApiResultTestSerializableObject( 'ok' );
+               $obj->foobar = 'foobaz';
+               $this->assertTrue( $result->addValue( null, 'foo', $obj ) );
+               $this->assertSame( 2, $result->getSize() );
+
+               $result = new ApiResult( 8388608 );
+               $result2 = new ApiResult( 8388608 );
+               $result2->addValue( null, 'foo', 'bar' );
+               $result->addValue( null, 'baz', $result2 );
+               $this->assertSame( [
+                       'baz' => [
+                               'foo' => 'bar',
+                               ApiResult::META_TYPE => 'assoc',
+                       ],
+                       ApiResult::META_TYPE => 'assoc',
+               ], $result->getResultData() );
+
+               $result = new ApiResult( 8388608 );
+               $result->addValue( null, 'foo', "foo\x80bar" );
+               $result->addValue( null, 'bar', "a\xcc\x81" );
+               $result->addValue( null, 'baz', 74 );
+               $result->addValue( null, null, "foo\x80bar" );
+               $result->addValue( null, null, "a\xcc\x81" );
+               $this->assertSame( [
+                       'foo' => "foo\xef\xbf\xbdbar",
+                       'bar' => "\xc3\xa1",
+                       'baz' => 74,
+                       0 => "foo\xef\xbf\xbdbar",
+                       1 => "\xc3\xa1",
+                       ApiResult::META_TYPE => 'assoc',
+               ], $result->getResultData() );
+
+               $result = new ApiResult( 8388608 );
+               $obj = new stdClass;
+               $obj->{'1'} = 'one';
+               $arr = [];
+               $result->addValue( $arr, 'foo', $obj );
+               $this->assertSame( [
+                       'foo' => [
+                               1 => 'one',
+                               ApiResult::META_TYPE => 'assoc',
+                       ],
+                       ApiResult::META_TYPE => 'assoc',
+               ], $result->getResultData() );
+       }
+
+       /**
+        * @covers ApiResult
+        */
+       public function testMetadata() {
+               $arr = [ 'foo' => [ 'bar' => [] ] ];
+               $result = new ApiResult( 8388608 );
+               $result->addValue( null, 'foo', [ 'bar' => [] ] );
+
+               $expect = [
+                       'foo' => [
+                               'bar' => [
+                                       ApiResult::META_INDEXED_TAG_NAME => 'ritn',
+                                       ApiResult::META_TYPE => 'default',
+                               ],
+                               ApiResult::META_INDEXED_TAG_NAME => 'ritn',
+                               ApiResult::META_TYPE => 'default',
+                       ],
+                       ApiResult::META_SUBELEMENTS => [ 'foo', 'bar' ],
+                       ApiResult::META_INDEXED_TAG_NAME => 'itn',
+                       ApiResult::META_PRESERVE_KEYS => [ 'foo', 'bar' ],
+                       ApiResult::META_TYPE => 'array',
+               ];
+
+               ApiResult::setSubelementsList( $arr, 'foo' );
+               ApiResult::setSubelementsList( $arr, [ 'bar', 'baz' ] );
+               ApiResult::unsetSubelementsList( $arr, 'baz' );
+               ApiResult::setIndexedTagNameRecursive( $arr, 'ritn' );
+               ApiResult::setIndexedTagName( $arr, 'itn' );
+               ApiResult::setPreserveKeysList( $arr, 'foo' );
+               ApiResult::setPreserveKeysList( $arr, [ 'bar', 'baz' ] );
+               ApiResult::unsetPreserveKeysList( $arr, 'baz' );
+               ApiResult::setArrayTypeRecursive( $arr, 'default' );
+               ApiResult::setArrayType( $arr, 'array' );
+               $this->assertSame( $expect, $arr );
+
+               $result->addSubelementsList( null, 'foo' );
+               $result->addSubelementsList( null, [ 'bar', 'baz' ] );
+               $result->removeSubelementsList( null, 'baz' );
+               $result->addIndexedTagNameRecursive( null, 'ritn' );
+               $result->addIndexedTagName( null, 'itn' );
+               $result->addPreserveKeysList( null, 'foo' );
+               $result->addPreserveKeysList( null, [ 'bar', 'baz' ] );
+               $result->removePreserveKeysList( null, 'baz' );
+               $result->addArrayTypeRecursive( null, 'default' );
+               $result->addArrayType( null, 'array' );
+               $this->assertEquals( $expect, $result->getResultData() );
+
+               $arr = [ 'foo' => [ 'bar' => [] ] ];
+               $expect = [
+                       'foo' => [
+                               'bar' => [
+                                       ApiResult::META_TYPE => 'kvp',
+                                       ApiResult::META_KVP_KEY_NAME => 'key',
+                               ],
+                               ApiResult::META_TYPE => 'kvp',
+                               ApiResult::META_KVP_KEY_NAME => 'key',
+                       ],
+                       ApiResult::META_TYPE => 'BCkvp',
+                       ApiResult::META_KVP_KEY_NAME => 'bc',
+               ];
+               ApiResult::setArrayTypeRecursive( $arr, 'kvp', 'key' );
+               ApiResult::setArrayType( $arr, 'BCkvp', 'bc' );
+               $this->assertSame( $expect, $arr );
+       }
+
+       /**
+        * @covers ApiResult
+        */
+       public function testUtilityFunctions() {
+               $arr = [
+                       'foo' => [
+                               'bar' => [ '_dummy' => 'foobaz' ],
+                               'bar2' => (object)[ '_dummy' => 'foobaz' ],
+                               'x' => 'ok',
+                               '_dummy' => 'foobaz',
+                       ],
+                       'foo2' => (object)[
+                               'bar' => [ '_dummy' => 'foobaz' ],
+                               'bar2' => (object)[ '_dummy' => 'foobaz' ],
+                               'x' => 'ok',
+                               '_dummy' => 'foobaz',
+                       ],
+                       ApiResult::META_SUBELEMENTS => [ 'foo', 'bar' ],
+                       ApiResult::META_INDEXED_TAG_NAME => 'itn',
+                       ApiResult::META_PRESERVE_KEYS => [ 'foo', 'bar', '_dummy2', 0 ],
+                       ApiResult::META_TYPE => 'array',
+                       '_dummy' => 'foobaz',
+                       '_dummy2' => 'foobaz!',
+               ];
+               $this->assertEquals( [
+                       'foo' => [
+                               'bar' => [],
+                               'bar2' => (object)[],
+                               'x' => 'ok',
+                       ],
+                       'foo2' => (object)[
+                               'bar' => [],
+                               'bar2' => (object)[],
+                               'x' => 'ok',
+                       ],
+                       '_dummy2' => 'foobaz!',
+               ], ApiResult::stripMetadata( $arr ), 'ApiResult::stripMetadata' );
+
+               $metadata = [];
+               $data = ApiResult::stripMetadataNonRecursive( $arr, $metadata );
+               $this->assertEquals( [
+                       'foo' => [
+                               'bar' => [ '_dummy' => 'foobaz' ],
+                               'bar2' => (object)[ '_dummy' => 'foobaz' ],
+                               'x' => 'ok',
+                               '_dummy' => 'foobaz',
+                       ],
+                       'foo2' => (object)[
+                               'bar' => [ '_dummy' => 'foobaz' ],
+                               'bar2' => (object)[ '_dummy' => 'foobaz' ],
+                               'x' => 'ok',
+                               '_dummy' => 'foobaz',
+                       ],
+                       '_dummy2' => 'foobaz!',
+               ], $data, 'ApiResult::stripMetadataNonRecursive ($data)' );
+               $this->assertEquals( [
+                       ApiResult::META_SUBELEMENTS => [ 'foo', 'bar' ],
+                       ApiResult::META_INDEXED_TAG_NAME => 'itn',
+                       ApiResult::META_PRESERVE_KEYS => [ 'foo', 'bar', '_dummy2', 0 ],
+                       ApiResult::META_TYPE => 'array',
+                       '_dummy' => 'foobaz',
+               ], $metadata, 'ApiResult::stripMetadataNonRecursive ($metadata)' );
+
+               $metadata = null;
+               $data = ApiResult::stripMetadataNonRecursive( (object)$arr, $metadata );
+               $this->assertEquals( (object)[
+                       'foo' => [
+                               'bar' => [ '_dummy' => 'foobaz' ],
+                               'bar2' => (object)[ '_dummy' => 'foobaz' ],
+                               'x' => 'ok',
+                               '_dummy' => 'foobaz',
+                       ],
+                       'foo2' => (object)[
+                               'bar' => [ '_dummy' => 'foobaz' ],
+                               'bar2' => (object)[ '_dummy' => 'foobaz' ],
+                               'x' => 'ok',
+                               '_dummy' => 'foobaz',
+                       ],
+                       '_dummy2' => 'foobaz!',
+               ], $data, 'ApiResult::stripMetadataNonRecursive on object ($data)' );
+               $this->assertEquals( [
+                       ApiResult::META_SUBELEMENTS => [ 'foo', 'bar' ],
+                       ApiResult::META_INDEXED_TAG_NAME => 'itn',
+                       ApiResult::META_PRESERVE_KEYS => [ 'foo', 'bar', '_dummy2', 0 ],
+                       ApiResult::META_TYPE => 'array',
+                       '_dummy' => 'foobaz',
+               ], $metadata, 'ApiResult::stripMetadataNonRecursive on object ($metadata)' );
+       }
+
+       /**
+        * @covers ApiResult
+        * @dataProvider provideTransformations
+        * @param string $label
+        * @param array $input
+        * @param array $transforms
+        * @param array|Exception $expect
+        */
+       public function testTransformations( $label, $input, $transforms, $expect ) {
+               $result = new ApiResult( false );
+               $result->addValue( null, 'test', $input );
+
+               if ( $expect instanceof Exception ) {
+                       try {
+                               $output = $result->getResultData( 'test', $transforms );
+                               $this->fail( 'Expected exception not thrown', $label );
+                       } catch ( Exception $ex ) {
+                               $this->assertEquals( $ex, $expect, $label );
+                       }
+               } else {
+                       $output = $result->getResultData( 'test', $transforms );
+                       $this->assertEquals( $expect, $output, $label );
+               }
+       }
+
+       public function provideTransformations() {
+               $kvp = function ( $keyKey, $key, $valKey, $value ) {
+                       return [
+                               $keyKey => $key,
+                               $valKey => $value,
+                               ApiResult::META_PRESERVE_KEYS => [ $keyKey ],
+                               ApiResult::META_CONTENT => $valKey,
+                               ApiResult::META_TYPE => 'assoc',
+                       ];
+               };
+               $typeArr = [
+                       'defaultArray' => [ 2 => 'a', 0 => 'b', 1 => 'c' ],
+                       'defaultAssoc' => [ 'x' => 'a', 1 => 'b', 0 => 'c' ],
+                       'defaultAssoc2' => [ 2 => 'a', 3 => 'b', 0 => 'c' ],
+                       'array' => [ 'x' => 'a', 1 => 'b', 0 => 'c', ApiResult::META_TYPE => 'array' ],
+                       'BCarray' => [ 'x' => 'a', 1 => 'b', 0 => 'c', ApiResult::META_TYPE => 'BCarray' ],
+                       'BCassoc' => [ 'a', 'b', 'c', ApiResult::META_TYPE => 'BCassoc' ],
+                       'assoc' => [ 2 => 'a', 0 => 'b', 1 => 'c', ApiResult::META_TYPE => 'assoc' ],
+                       'kvp' => [ 'x' => 'a', 'y' => 'b', 'z' => [ 'c' ], ApiResult::META_TYPE => 'kvp' ],
+                       'BCkvp' => [ 'x' => 'a', 'y' => 'b',
+                               ApiResult::META_TYPE => 'BCkvp',
+                               ApiResult::META_KVP_KEY_NAME => 'key',
+                       ],
+                       'kvpmerge' => [ 'x' => 'a', 'y' => [ 'b' ], 'z' => [ 'c' => 'd' ],
+                               ApiResult::META_TYPE => 'kvp',
+                               ApiResult::META_KVP_MERGE => true,
+                       ],
+                       'emptyDefault' => [ '_dummy' => 1 ],
+                       'emptyAssoc' => [ '_dummy' => 1, ApiResult::META_TYPE => 'assoc' ],
+                       '_dummy' => 1,
+                       ApiResult::META_PRESERVE_KEYS => [ '_dummy' ],
+               ];
+               $stripArr = [
+                       'foo' => [
+                               'bar' => [ '_dummy' => 'foobaz' ],
+                               'baz' => [
+                                       ApiResult::META_SUBELEMENTS => [ 'foo', 'bar' ],
+                                       ApiResult::META_INDEXED_TAG_NAME => 'itn',
+                                       ApiResult::META_PRESERVE_KEYS => [ 'foo', 'bar', '_dummy2', 0 ],
+                                       ApiResult::META_TYPE => 'array',
+                               ],
+                               'x' => 'ok',
+                               '_dummy' => 'foobaz',
+                       ],
+                       ApiResult::META_SUBELEMENTS => [ 'foo', 'bar' ],
+                       ApiResult::META_INDEXED_TAG_NAME => 'itn',
+                       ApiResult::META_PRESERVE_KEYS => [ 'foo', 'bar', '_dummy2', 0 ],
+                       ApiResult::META_TYPE => 'array',
+                       '_dummy' => 'foobaz',
+                       '_dummy2' => 'foobaz!',
+               ];
+
+               return [
+                       [
+                               'BC: META_BC_BOOLS',
+                               [
+                                       'BCtrue' => true,
+                                       'BCfalse' => false,
+                                       'true' => true,
+                                       'false' => false,
+                                       ApiResult::META_BC_BOOLS => [ 0, 'true', 'false' ],
+                               ],
+                               [ 'BC' => [] ],
+                               [
+                                       'BCtrue' => '',
+                                       'true' => true,
+                                       'false' => false,
+                                       ApiResult::META_BC_BOOLS => [ 0, 'true', 'false' ],
+                               ]
+                       ],
+                       [
+                               'BC: META_BC_SUBELEMENTS',
+                               [
+                                       'bc' => 'foo',
+                                       'nobc' => 'bar',
+                                       ApiResult::META_BC_SUBELEMENTS => [ 'bc' ],
+                               ],
+                               [ 'BC' => [] ],
+                               [
+                                       'bc' => [
+                                               '*' => 'foo',
+                                               ApiResult::META_CONTENT => '*',
+                                               ApiResult::META_TYPE => 'assoc',
+                                       ],
+                                       'nobc' => 'bar',
+                                       ApiResult::META_BC_SUBELEMENTS => [ 'bc' ],
+                               ],
+                       ],
+                       [
+                               'BC: META_CONTENT',
+                               [
+                                       'content' => '!!!',
+                                       ApiResult::META_CONTENT => 'content',
+                               ],
+                               [ 'BC' => [] ],
+                               [
+                                       '*' => '!!!',
+                                       ApiResult::META_CONTENT => '*',
+                               ],
+                       ],
+                       [
+                               'BC: BCkvp type',
+                               [
+                                       'foo' => 'foo value',
+                                       'bar' => 'bar value',
+                                       '_baz' => 'baz value',
+                                       ApiResult::META_TYPE => 'BCkvp',
+                                       ApiResult::META_KVP_KEY_NAME => 'key',
+                                       ApiResult::META_PRESERVE_KEYS => [ '_baz' ],
+                               ],
+                               [ 'BC' => [] ],
+                               [
+                                       $kvp( 'key', 'foo', '*', 'foo value' ),
+                                       $kvp( 'key', 'bar', '*', 'bar value' ),
+                                       $kvp( 'key', '_baz', '*', 'baz value' ),
+                                       ApiResult::META_TYPE => 'array',
+                                       ApiResult::META_KVP_KEY_NAME => 'key',
+                                       ApiResult::META_PRESERVE_KEYS => [ '_baz' ],
+                               ],
+                       ],
+                       [
+                               'BC: BCarray type',
+                               [
+                                       ApiResult::META_TYPE => 'BCarray',
+                               ],
+                               [ 'BC' => [] ],
+                               [
+                                       ApiResult::META_TYPE => 'default',
+                               ],
+                       ],
+                       [
+                               'BC: BCassoc type',
+                               [
+                                       ApiResult::META_TYPE => 'BCassoc',
+                               ],
+                               [ 'BC' => [] ],
+                               [
+                                       ApiResult::META_TYPE => 'default',
+                               ],
+                       ],
+                       [
+                               'BC: BCkvp exception',
+                               [
+                                       ApiResult::META_TYPE => 'BCkvp',
+                               ],
+                               [ 'BC' => [] ],
+                               new UnexpectedValueException(
+                                       'Type "BCkvp" used without setting ApiResult::META_KVP_KEY_NAME metadata item'
+                               ),
+                       ],
+                       [
+                               'BC: nobool, no*, nosub',
+                               [
+                                       'true' => true,
+                                       'false' => false,
+                                       'content' => 'content',
+                                       ApiResult::META_CONTENT => 'content',
+                                       'bc' => 'foo',
+                                       ApiResult::META_BC_SUBELEMENTS => [ 'bc' ],
+                                       'BCarray' => [ ApiResult::META_TYPE => 'BCarray' ],
+                                       'BCassoc' => [ ApiResult::META_TYPE => 'BCassoc' ],
+                                       'BCkvp' => [
+                                               'foo' => 'foo value',
+                                               'bar' => 'bar value',
+                                               '_baz' => 'baz value',
+                                               ApiResult::META_TYPE => 'BCkvp',
+                                               ApiResult::META_KVP_KEY_NAME => 'key',
+                                               ApiResult::META_PRESERVE_KEYS => [ '_baz' ],
+                                       ],
+                               ],
+                               [ 'BC' => [ 'nobool', 'no*', 'nosub' ] ],
+                               [
+                                       'true' => true,
+                                       'false' => false,
+                                       'content' => 'content',
+                                       'bc' => 'foo',
+                                       'BCarray' => [ ApiResult::META_TYPE => 'default' ],
+                                       'BCassoc' => [ ApiResult::META_TYPE => 'default' ],
+                                       'BCkvp' => [
+                                               $kvp( 'key', 'foo', '*', 'foo value' ),
+                                               $kvp( 'key', 'bar', '*', 'bar value' ),
+                                               $kvp( 'key', '_baz', '*', 'baz value' ),
+                                               ApiResult::META_TYPE => 'array',
+                                               ApiResult::META_KVP_KEY_NAME => 'key',
+                                               ApiResult::META_PRESERVE_KEYS => [ '_baz' ],
+                                       ],
+                                       ApiResult::META_CONTENT => 'content',
+                                       ApiResult::META_BC_SUBELEMENTS => [ 'bc' ],
+                               ],
+                       ],
+
+                       [
+                               'Types: Normal transform',
+                               $typeArr,
+                               [ 'Types' => [] ],
+                               [
+                                       'defaultArray' => [ 'b', 'c', 'a', ApiResult::META_TYPE => 'array' ],
+                                       'defaultAssoc' => [ 'x' => 'a', 1 => 'b', 0 => 'c', ApiResult::META_TYPE => 'assoc' ],
+                                       'defaultAssoc2' => [ 2 => 'a', 3 => 'b', 0 => 'c', ApiResult::META_TYPE => 'assoc' ],
+                                       'array' => [ 'a', 'c', 'b', ApiResult::META_TYPE => 'array' ],
+                                       'BCarray' => [ 'a', 'c', 'b', ApiResult::META_TYPE => 'array' ],
+                                       'BCassoc' => [ 'a', 'b', 'c', ApiResult::META_TYPE => 'assoc' ],
+                                       'assoc' => [ 2 => 'a', 0 => 'b', 1 => 'c', ApiResult::META_TYPE => 'assoc' ],
+                                       'kvp' => [ 'x' => 'a', 'y' => 'b',
+                                               'z' => [ 'c', ApiResult::META_TYPE => 'array' ],
+                                               ApiResult::META_TYPE => 'assoc'
+                                       ],
+                                       'BCkvp' => [ 'x' => 'a', 'y' => 'b',
+                                               ApiResult::META_TYPE => 'assoc',
+                                               ApiResult::META_KVP_KEY_NAME => 'key',
+                                       ],
+                                       'kvpmerge' => [
+                                               'x' => 'a',
+                                               'y' => [ 'b', ApiResult::META_TYPE => 'array' ],
+                                               'z' => [ 'c' => 'd', ApiResult::META_TYPE => 'assoc' ],
+                                               ApiResult::META_TYPE => 'assoc',
+                                               ApiResult::META_KVP_MERGE => true,
+                                       ],
+                                       'emptyDefault' => [ '_dummy' => 1, ApiResult::META_TYPE => 'array' ],
+                                       'emptyAssoc' => [ '_dummy' => 1, ApiResult::META_TYPE => 'assoc' ],
+                                       '_dummy' => 1,
+                                       ApiResult::META_PRESERVE_KEYS => [ '_dummy' ],
+                                       ApiResult::META_TYPE => 'assoc',
+                               ],
+                       ],
+                       [
+                               'Types: AssocAsObject',
+                               $typeArr,
+                               [ 'Types' => [ 'AssocAsObject' => true ] ],
+                               (object)[
+                                       'defaultArray' => [ 'b', 'c', 'a', ApiResult::META_TYPE => 'array' ],
+                                       'defaultAssoc' => (object)[ 'x' => 'a',
+                                               1 => 'b', 0 => 'c', ApiResult::META_TYPE => 'assoc'
+                                       ],
+                                       'defaultAssoc2' => (object)[ 2 => 'a', 3 => 'b',
+                                               0 => 'c', ApiResult::META_TYPE => 'assoc'
+                                       ],
+                                       'array' => [ 'a', 'c', 'b', ApiResult::META_TYPE => 'array' ],
+                                       'BCarray' => [ 'a', 'c', 'b', ApiResult::META_TYPE => 'array' ],
+                                       'BCassoc' => (object)[ 'a', 'b', 'c', ApiResult::META_TYPE => 'assoc' ],
+                                       'assoc' => (object)[ 2 => 'a', 0 => 'b', 1 => 'c', ApiResult::META_TYPE => 'assoc' ],
+                                       'kvp' => (object)[ 'x' => 'a', 'y' => 'b',
+                                               'z' => [ 'c', ApiResult::META_TYPE => 'array' ],
+                                               ApiResult::META_TYPE => 'assoc'
+                                       ],
+                                       'BCkvp' => (object)[ 'x' => 'a', 'y' => 'b',
+                                               ApiResult::META_TYPE => 'assoc',
+                                               ApiResult::META_KVP_KEY_NAME => 'key',
+                                       ],
+                                       'kvpmerge' => (object)[
+                                               'x' => 'a',
+                                               'y' => [ 'b', ApiResult::META_TYPE => 'array' ],
+                                               'z' => (object)[ 'c' => 'd', ApiResult::META_TYPE => 'assoc' ],
+                                               ApiResult::META_TYPE => 'assoc',
+                                               ApiResult::META_KVP_MERGE => true,
+                                       ],
+                                       'emptyDefault' => [ '_dummy' => 1, ApiResult::META_TYPE => 'array' ],
+                                       'emptyAssoc' => (object)[ '_dummy' => 1, ApiResult::META_TYPE => 'assoc' ],
+                                       '_dummy' => 1,
+                                       ApiResult::META_PRESERVE_KEYS => [ '_dummy' ],
+                                       ApiResult::META_TYPE => 'assoc',
+                               ],
+                       ],
+                       [
+                               'Types: ArmorKVP',
+                               $typeArr,
+                               [ 'Types' => [ 'ArmorKVP' => 'name' ] ],
+                               [
+                                       'defaultArray' => [ 'b', 'c', 'a', ApiResult::META_TYPE => 'array' ],
+                                       'defaultAssoc' => [ 'x' => 'a', 1 => 'b', 0 => 'c', ApiResult::META_TYPE => 'assoc' ],
+                                       'defaultAssoc2' => [ 2 => 'a', 3 => 'b', 0 => 'c', ApiResult::META_TYPE => 'assoc' ],
+                                       'array' => [ 'a', 'c', 'b', ApiResult::META_TYPE => 'array' ],
+                                       'BCarray' => [ 'a', 'c', 'b', ApiResult::META_TYPE => 'array' ],
+                                       'BCassoc' => [ 'a', 'b', 'c', ApiResult::META_TYPE => 'assoc' ],
+                                       'assoc' => [ 2 => 'a', 0 => 'b', 1 => 'c', ApiResult::META_TYPE => 'assoc' ],
+                                       'kvp' => [
+                                               $kvp( 'name', 'x', 'value', 'a' ),
+                                               $kvp( 'name', 'y', 'value', 'b' ),
+                                               $kvp( 'name', 'z', 'value', [ 'c', ApiResult::META_TYPE => 'array' ] ),
+                                               ApiResult::META_TYPE => 'array'
+                                       ],
+                                       'BCkvp' => [
+                                               $kvp( 'key', 'x', 'value', 'a' ),
+                                               $kvp( 'key', 'y', 'value', 'b' ),
+                                               ApiResult::META_TYPE => 'array',
+                                               ApiResult::META_KVP_KEY_NAME => 'key',
+                                       ],
+                                       'kvpmerge' => [
+                                               $kvp( 'name', 'x', 'value', 'a' ),
+                                               $kvp( 'name', 'y', 'value', [ 'b', ApiResult::META_TYPE => 'array' ] ),
+                                               [
+                                                       'name' => 'z',
+                                                       'c' => 'd',
+                                                       ApiResult::META_TYPE => 'assoc',
+                                                       ApiResult::META_PRESERVE_KEYS => [ 'name' ]
+                                               ],
+                                               ApiResult::META_TYPE => 'array',
+                                               ApiResult::META_KVP_MERGE => true,
+                                       ],
+                                       'emptyDefault' => [ '_dummy' => 1, ApiResult::META_TYPE => 'array' ],
+                                       'emptyAssoc' => [ '_dummy' => 1, ApiResult::META_TYPE => 'assoc' ],
+                                       '_dummy' => 1,
+                                       ApiResult::META_PRESERVE_KEYS => [ '_dummy' ],
+                                       ApiResult::META_TYPE => 'assoc',
+                               ],
+                       ],
+                       [
+                               'Types: ArmorKVP + BC',
+                               $typeArr,
+                               [ 'BC' => [], 'Types' => [ 'ArmorKVP' => 'name' ] ],
+                               [
+                                       'defaultArray' => [ 'b', 'c', 'a', ApiResult::META_TYPE => 'array' ],
+                                       'defaultAssoc' => [ 'x' => 'a', 1 => 'b', 0 => 'c', ApiResult::META_TYPE => 'assoc' ],
+                                       'defaultAssoc2' => [ 2 => 'a', 3 => 'b', 0 => 'c', ApiResult::META_TYPE => 'assoc' ],
+                                       'array' => [ 'a', 'c', 'b', ApiResult::META_TYPE => 'array' ],
+                                       'BCarray' => [ 'x' => 'a', 1 => 'b', 0 => 'c', ApiResult::META_TYPE => 'assoc' ],
+                                       'BCassoc' => [ 'a', 'b', 'c', ApiResult::META_TYPE => 'array' ],
+                                       'assoc' => [ 2 => 'a', 0 => 'b', 1 => 'c', ApiResult::META_TYPE => 'assoc' ],
+                                       'kvp' => [
+                                               $kvp( 'name', 'x', '*', 'a' ),
+                                               $kvp( 'name', 'y', '*', 'b' ),
+                                               $kvp( 'name', 'z', '*', [ 'c', ApiResult::META_TYPE => 'array' ] ),
+                                               ApiResult::META_TYPE => 'array'
+                                       ],
+                                       'BCkvp' => [
+                                               $kvp( 'key', 'x', '*', 'a' ),
+                                               $kvp( 'key', 'y', '*', 'b' ),
+                                               ApiResult::META_TYPE => 'array',
+                                               ApiResult::META_KVP_KEY_NAME => 'key',
+                                       ],
+                                       'kvpmerge' => [
+                                               $kvp( 'name', 'x', '*', 'a' ),
+                                               $kvp( 'name', 'y', '*', [ 'b', ApiResult::META_TYPE => 'array' ] ),
+                                               [
+                                                       'name' => 'z',
+                                                       'c' => 'd',
+                                                       ApiResult::META_TYPE => 'assoc',
+                                                       ApiResult::META_PRESERVE_KEYS => [ 'name' ] ],
+                                               ApiResult::META_TYPE => 'array',
+                                               ApiResult::META_KVP_MERGE => true,
+                                       ],
+                                       'emptyDefault' => [ '_dummy' => 1, ApiResult::META_TYPE => 'array' ],
+                                       'emptyAssoc' => [ '_dummy' => 1, ApiResult::META_TYPE => 'assoc' ],
+                                       '_dummy' => 1,
+                                       ApiResult::META_PRESERVE_KEYS => [ '_dummy' ],
+                                       ApiResult::META_TYPE => 'assoc',
+                               ],
+                       ],
+                       [
+                               'Types: ArmorKVP + AssocAsObject',
+                               $typeArr,
+                               [ 'Types' => [ 'ArmorKVP' => 'name', 'AssocAsObject' => true ] ],
+                               (object)[
+                                       'defaultArray' => [ 'b', 'c', 'a', ApiResult::META_TYPE => 'array' ],
+                                       'defaultAssoc' => (object)[ 'x' => 'a', 1 => 'b',
+                                               0 => 'c', ApiResult::META_TYPE => 'assoc'
+                                       ],
+                                       'defaultAssoc2' => (object)[ 2 => 'a', 3 => 'b',
+                                               0 => 'c', ApiResult::META_TYPE => 'assoc'
+                                       ],
+                                       'array' => [ 'a', 'c', 'b', ApiResult::META_TYPE => 'array' ],
+                                       'BCarray' => [ 'a', 'c', 'b', ApiResult::META_TYPE => 'array' ],
+                                       'BCassoc' => (object)[ 'a', 'b', 'c', ApiResult::META_TYPE => 'assoc' ],
+                                       'assoc' => (object)[ 2 => 'a', 0 => 'b', 1 => 'c', ApiResult::META_TYPE => 'assoc' ],
+                                       'kvp' => [
+                                               (object)$kvp( 'name', 'x', 'value', 'a' ),
+                                               (object)$kvp( 'name', 'y', 'value', 'b' ),
+                                               (object)$kvp( 'name', 'z', 'value', [ 'c', ApiResult::META_TYPE => 'array' ] ),
+                                               ApiResult::META_TYPE => 'array'
+                                       ],
+                                       'BCkvp' => [
+                                               (object)$kvp( 'key', 'x', 'value', 'a' ),
+                                               (object)$kvp( 'key', 'y', 'value', 'b' ),
+                                               ApiResult::META_TYPE => 'array',
+                                               ApiResult::META_KVP_KEY_NAME => 'key',
+                                       ],
+                                       'kvpmerge' => [
+                                               (object)$kvp( 'name', 'x', 'value', 'a' ),
+                                               (object)$kvp( 'name', 'y', 'value', [ 'b', ApiResult::META_TYPE => 'array' ] ),
+                                               (object)[
+                                                       'name' => 'z',
+                                                       'c' => 'd',
+                                                       ApiResult::META_TYPE => 'assoc',
+                                                       ApiResult::META_PRESERVE_KEYS => [ 'name' ]
+                                               ],
+                                               ApiResult::META_TYPE => 'array',
+                                               ApiResult::META_KVP_MERGE => true,
+                                       ],
+                                       'emptyDefault' => [ '_dummy' => 1, ApiResult::META_TYPE => 'array' ],
+                                       'emptyAssoc' => (object)[ '_dummy' => 1, ApiResult::META_TYPE => 'assoc' ],
+                                       '_dummy' => 1,
+                                       ApiResult::META_PRESERVE_KEYS => [ '_dummy' ],
+                                       ApiResult::META_TYPE => 'assoc',
+                               ],
+                       ],
+                       [
+                               'Types: BCkvp exception',
+                               [
+                                       ApiResult::META_TYPE => 'BCkvp',
+                               ],
+                               [ 'Types' => [] ],
+                               new UnexpectedValueException(
+                                       'Type "BCkvp" used without setting ApiResult::META_KVP_KEY_NAME metadata item'
+                               ),
+                       ],
+
+                       [
+                               'Strip: With ArmorKVP + AssocAsObject transforms',
+                               $typeArr,
+                               [ 'Types' => [ 'ArmorKVP' => 'name', 'AssocAsObject' => true ], 'Strip' => 'all' ],
+                               (object)[
+                                       'defaultArray' => [ 'b', 'c', 'a' ],
+                                       'defaultAssoc' => (object)[ 'x' => 'a', 1 => 'b', 0 => 'c' ],
+                                       'defaultAssoc2' => (object)[ 2 => 'a', 3 => 'b', 0 => 'c' ],
+                                       'array' => [ 'a', 'c', 'b' ],
+                                       'BCarray' => [ 'a', 'c', 'b' ],
+                                       'BCassoc' => (object)[ 'a', 'b', 'c' ],
+                                       'assoc' => (object)[ 2 => 'a', 0 => 'b', 1 => 'c' ],
+                                       'kvp' => [
+                                               (object)[ 'name' => 'x', 'value' => 'a' ],
+                                               (object)[ 'name' => 'y', 'value' => 'b' ],
+                                               (object)[ 'name' => 'z', 'value' => [ 'c' ] ],
+                                       ],
+                                       'BCkvp' => [
+                                               (object)[ 'key' => 'x', 'value' => 'a' ],
+                                               (object)[ 'key' => 'y', 'value' => 'b' ],
+                                       ],
+                                       'kvpmerge' => [
+                                               (object)[ 'name' => 'x', 'value' => 'a' ],
+                                               (object)[ 'name' => 'y', 'value' => [ 'b' ] ],
+                                               (object)[ 'name' => 'z', 'c' => 'd' ],
+                                       ],
+                                       'emptyDefault' => [],
+                                       'emptyAssoc' => (object)[],
+                                       '_dummy' => 1,
+                               ],
+                       ],
+
+                       [
+                               'Strip: all',
+                               $stripArr,
+                               [ 'Strip' => 'all' ],
+                               [
+                                       'foo' => [
+                                               'bar' => [],
+                                               'baz' => [],
+                                               'x' => 'ok',
+                                       ],
+                                       '_dummy2' => 'foobaz!',
+                               ],
+                       ],
+                       [
+                               'Strip: base',
+                               $stripArr,
+                               [ 'Strip' => 'base' ],
+                               [
+                                       'foo' => [
+                                               'bar' => [ '_dummy' => 'foobaz' ],
+                                               'baz' => [
+                                                       ApiResult::META_SUBELEMENTS => [ 'foo', 'bar' ],
+                                                       ApiResult::META_INDEXED_TAG_NAME => 'itn',
+                                                       ApiResult::META_PRESERVE_KEYS => [ 'foo', 'bar', '_dummy2', 0 ],
+                                                       ApiResult::META_TYPE => 'array',
+                                               ],
+                                               'x' => 'ok',
+                                               '_dummy' => 'foobaz',
+                                       ],
+                                       '_dummy2' => 'foobaz!',
+                               ],
+                       ],
+                       [
+                               'Strip: bc',
+                               $stripArr,
+                               [ 'Strip' => 'bc' ],
+                               [
+                                       'foo' => [
+                                               'bar' => [],
+                                               'baz' => [
+                                                       ApiResult::META_SUBELEMENTS => [ 'foo', 'bar' ],
+                                                       ApiResult::META_INDEXED_TAG_NAME => 'itn',
+                                               ],
+                                               'x' => 'ok',
+                                       ],
+                                       '_dummy2' => 'foobaz!',
+                                       ApiResult::META_SUBELEMENTS => [ 'foo', 'bar' ],
+                                       ApiResult::META_INDEXED_TAG_NAME => 'itn',
+                               ],
+                       ],
+
+                       [
+                               'Custom transform',
+                               [
+                                       'foo' => '?',
+                                       'bar' => '?',
+                                       '_dummy' => '?',
+                                       '_dummy2' => '?',
+                                       '_dummy3' => '?',
+                                       ApiResult::META_CONTENT => 'foo',
+                                       ApiResult::META_PRESERVE_KEYS => [ '_dummy2', '_dummy3' ],
+                               ],
+                               [
+                                       'Custom' => [ $this, 'customTransform' ],
+                                       'BC' => [],
+                                       'Types' => [],
+                                       'Strip' => 'all'
+                               ],
+                               [
+                                       '*' => 'FOO',
+                                       'bar' => 'BAR',
+                                       'baz' => [ 'a', 'b' ],
+                                       '_dummy2' => '_DUMMY2',
+                                       '_dummy3' => '_DUMMY3',
+                                       ApiResult::META_CONTENT => 'bar',
+                               ],
+                       ],
+               ];
+       }
+
+       /**
+        * Custom transformer for testTransformations
+        * @param array &$data
+        * @param array &$metadata
+        */
+       public function customTransform( &$data, &$metadata ) {
+               // Prevent recursion
+               if ( isset( $metadata['_added'] ) ) {
+                       $metadata[ApiResult::META_TYPE] = 'array';
+                       return;
+               }
+
+               foreach ( $data as $k => $v ) {
+                       $data[$k] = strtoupper( $k );
+               }
+               $data['baz'] = [ '_added' => 1, 'z' => 'b', 'y' => 'a' ];
+               $metadata[ApiResult::META_PRESERVE_KEYS][0] = '_dummy';
+               $data[ApiResult::META_CONTENT] = 'bar';
+       }
+
+       /**
+        * @covers ApiResult
+        */
+       public function testAddMetadataToResultVars() {
+               $arr = [
+                       'a' => "foo",
+                       'b' => false,
+                       'c' => 10,
+                       'sequential_numeric_keys' => [ 'a', 'b', 'c' ],
+                       'non_sequential_numeric_keys' => [ 'a', 'b', 4 => 'c' ],
+                       'string_keys' => [
+                               'one' => 1,
+                               'two' => 2
+                       ],
+                       'object_sequential_keys' => (object)[ 'a', 'b', 'c' ],
+                       '_type' => "should be overwritten in result",
+               ];
+               $this->assertSame( [
+                       ApiResult::META_TYPE => 'kvp',
+                       ApiResult::META_KVP_KEY_NAME => 'key',
+                       ApiResult::META_PRESERVE_KEYS => [
+                               'a', 'b', 'c',
+                               'sequential_numeric_keys', 'non_sequential_numeric_keys',
+                               'string_keys', 'object_sequential_keys'
+                       ],
+                       ApiResult::META_BC_BOOLS => [ 'b' ],
+                       ApiResult::META_INDEXED_TAG_NAME => 'var',
+                       'a' => "foo",
+                       'b' => false,
+                       'c' => 10,
+                       'sequential_numeric_keys' => [
+                               ApiResult::META_TYPE => 'array',
+                               ApiResult::META_BC_BOOLS => [],
+                               ApiResult::META_INDEXED_TAG_NAME => 'value',
+                               0 => 'a',
+                               1 => 'b',
+                               2 => 'c',
+                       ],
+                       'non_sequential_numeric_keys' => [
+                               ApiResult::META_TYPE => 'kvp',
+                               ApiResult::META_KVP_KEY_NAME => 'key',
+                               ApiResult::META_PRESERVE_KEYS => [ 0, 1, 4 ],
+                               ApiResult::META_BC_BOOLS => [],
+                               ApiResult::META_INDEXED_TAG_NAME => 'var',
+                               0 => 'a',
+                               1 => 'b',
+                               4 => 'c',
+                       ],
+                       'string_keys' => [
+                               ApiResult::META_TYPE => 'kvp',
+                               ApiResult::META_KVP_KEY_NAME => 'key',
+                               ApiResult::META_PRESERVE_KEYS => [ 'one', 'two' ],
+                               ApiResult::META_BC_BOOLS => [],
+                               ApiResult::META_INDEXED_TAG_NAME => 'var',
+                               'one' => 1,
+                               'two' => 2,
+                       ],
+                       'object_sequential_keys' => [
+                               ApiResult::META_TYPE => 'kvp',
+                               ApiResult::META_KVP_KEY_NAME => 'key',
+                               ApiResult::META_PRESERVE_KEYS => [ 0, 1, 2 ],
+                               ApiResult::META_BC_BOOLS => [],
+                               ApiResult::META_INDEXED_TAG_NAME => 'var',
+                               0 => 'a',
+                               1 => 'b',
+                               2 => 'c',
+                       ],
+               ], ApiResult::addMetadataToResultVars( $arr ) );
+       }
+
+       public function testObjectSerialization() {
+               $arr = [];
+               ApiResult::setValue( $arr, 'foo', (object)[ 'a' => 1, 'b' => 2 ] );
+               $this->assertSame( [
+                       'a' => 1,
+                       'b' => 2,
+                       ApiResult::META_TYPE => 'assoc',
+               ], $arr['foo'] );
+
+               $arr = [];
+               ApiResult::setValue( $arr, 'foo', new ApiResultTestStringifiableObject() );
+               $this->assertSame( 'Ok', $arr['foo'] );
+
+               $arr = [];
+               ApiResult::setValue( $arr, 'foo', new ApiResultTestSerializableObject( 'Ok' ) );
+               $this->assertSame( 'Ok', $arr['foo'] );
+
+               try {
+                       $arr = [];
+                       ApiResult::setValue( $arr, 'foo', new ApiResultTestSerializableObject(
+                               new ApiResultTestStringifiableObject()
+                       ) );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( UnexpectedValueException $ex ) {
+                       $this->assertSame(
+                               'ApiResultTestSerializableObject::serializeForApiResult() ' .
+                                       'returned an object of class ApiResultTestStringifiableObject',
+                               $ex->getMessage(),
+                               'Expected exception'
+                       );
+               }
+
+               try {
+                       $arr = [];
+                       ApiResult::setValue( $arr, 'foo', new ApiResultTestSerializableObject( NAN ) );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( UnexpectedValueException $ex ) {
+                       $this->assertSame(
+                               'ApiResultTestSerializableObject::serializeForApiResult() ' .
+                                       'returned an invalid value: Cannot add non-finite floats to ApiResult',
+                               $ex->getMessage(),
+                               'Expected exception'
+                       );
+               }
+
+               $arr = [];
+               ApiResult::setValue( $arr, 'foo', new ApiResultTestSerializableObject(
+                       [
+                               'one' => new ApiResultTestStringifiableObject( '1' ),
+                               'two' => new ApiResultTestSerializableObject( 2 ),
+                       ]
+               ) );
+               $this->assertSame( [
+                       'one' => '1',
+                       'two' => 2,
+               ], $arr['foo'] );
+       }
+}
+
+class ApiResultTestStringifiableObject {
+       private $ret;
+
+       public function __construct( $ret = 'Ok' ) {
+               $this->ret = $ret;
+       }
+
+       public function __toString() {
+               return $this->ret;
+       }
+}
+
+class ApiResultTestSerializableObject {
+       private $ret;
+
+       public function __construct( $ret ) {
+               $this->ret = $ret;
+       }
+
+       public function __toString() {
+               return "Fail";
+       }
+
+       public function serializeForApiResult() {
+               return $this->ret;
+       }
+}
diff --git a/tests/phpunit/unit/includes/api/ApiUsageExceptionTest.php b/tests/phpunit/unit/includes/api/ApiUsageExceptionTest.php
new file mode 100644 (file)
index 0000000..51260a6
--- /dev/null
@@ -0,0 +1,44 @@
+<?php
+
+/**
+ * @covers ApiUsageException
+ */
+class ApiUsageExceptionTest extends \MediaWikiUnitTestCase {
+
+       public function testCreateWithStatusValue_CanGetAMessageObject() {
+               $messageKey = 'some-message-key';
+               $messageParameter = 'some-parameter';
+               $statusValue = new StatusValue();
+               $statusValue->fatal( $messageKey, $messageParameter );
+
+               $apiUsageException = new ApiUsageException( null, $statusValue );
+               /** @var \Message $gotMessage */
+               $gotMessage = $apiUsageException->getMessageObject();
+
+               $this->assertInstanceOf( \Message::class, $gotMessage );
+               $this->assertEquals( $messageKey, $gotMessage->getKey() );
+               $this->assertEquals( [ $messageParameter ], $gotMessage->getParams() );
+       }
+
+       public function testNewWithMessage_ThenGetMessageObject_ReturnsApiMessageWithProvidedData() {
+               $expectedMessage = new Message( 'some-message-key', [ 'some message parameter' ] );
+               $expectedCode = 'some-error-code';
+               $expectedData = [ 'some-error-data' ];
+
+               $apiUsageException = ApiUsageException::newWithMessage(
+                       null,
+                       $expectedMessage,
+                       $expectedCode,
+                       $expectedData
+               );
+               /** @var \ApiMessage $gotMessage */
+               $gotMessage = $apiUsageException->getMessageObject();
+
+               $this->assertInstanceOf( \ApiMessage::class, $gotMessage );
+               $this->assertEquals( $expectedMessage->getKey(), $gotMessage->getKey() );
+               $this->assertEquals( $expectedMessage->getParams(), $gotMessage->getParams() );
+               $this->assertEquals( $expectedCode, $gotMessage->getApiCode() );
+               $this->assertEquals( $expectedData, $gotMessage->getApiData() );
+       }
+
+}
diff --git a/tests/phpunit/unit/includes/auth/AbstractPreAuthenticationProviderTest.php b/tests/phpunit/unit/includes/auth/AbstractPreAuthenticationProviderTest.php
new file mode 100644 (file)
index 0000000..8ec3380
--- /dev/null
@@ -0,0 +1,45 @@
+<?php
+
+namespace MediaWiki\Auth;
+
+/**
+ * @group AuthManager
+ * @covers \MediaWiki\Auth\AbstractPreAuthenticationProvider
+ */
+class AbstractPreAuthenticationProviderTest extends \MediaWikiUnitTestCase {
+       public function testAbstractPreAuthenticationProvider() {
+               $user = \User::newFromName( 'UTSysop' );
+
+               $provider = $this->getMockForAbstractClass( AbstractPreAuthenticationProvider::class );
+
+               $this->assertEquals(
+                       [],
+                       $provider->getAuthenticationRequests( AuthManager::ACTION_LOGIN, [] )
+               );
+               $this->assertEquals(
+                       \StatusValue::newGood(),
+                       $provider->testForAuthentication( [] )
+               );
+               $this->assertEquals(
+                       \StatusValue::newGood(),
+                       $provider->testForAccountCreation( $user, $user, [] )
+               );
+               $this->assertEquals(
+                       \StatusValue::newGood(),
+                       $provider->testUserForCreation( $user, AuthManager::AUTOCREATE_SOURCE_SESSION )
+               );
+               $this->assertEquals(
+                       \StatusValue::newGood(),
+                       $provider->testUserForCreation( $user, false )
+               );
+               $this->assertEquals(
+                       \StatusValue::newGood(),
+                       $provider->testForAccountLink( $user )
+               );
+
+               $res = AuthenticationResponse::newPass();
+               $provider->postAuthentication( $user, $res );
+               $provider->postAccountCreation( $user, $user, $res );
+               $provider->postAccountLink( $user, $res );
+       }
+}
diff --git a/tests/phpunit/unit/includes/auth/AbstractSecondaryAuthenticationProviderTest.php b/tests/phpunit/unit/includes/auth/AbstractSecondaryAuthenticationProviderTest.php
new file mode 100644 (file)
index 0000000..e933cb8
--- /dev/null
@@ -0,0 +1,84 @@
+<?php
+
+namespace MediaWiki\Auth;
+
+/**
+ * @group AuthManager
+ * @covers \MediaWiki\Auth\AbstractSecondaryAuthenticationProvider
+ */
+class AbstractSecondaryAuthenticationProviderTest extends \MediaWikiUnitTestCase {
+       public function testAbstractSecondaryAuthenticationProvider() {
+               $user = \User::newFromName( 'UTSysop' );
+
+               $provider = $this->getMockForAbstractClass( AbstractSecondaryAuthenticationProvider::class );
+
+               try {
+                       $provider->continueSecondaryAuthentication( $user, [] );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( \BadMethodCallException $ex ) {
+               }
+
+               try {
+                       $provider->continueSecondaryAccountCreation( $user, $user, [] );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( \BadMethodCallException $ex ) {
+               }
+
+               $req = $this->getMockForAbstractClass( AuthenticationRequest::class );
+
+               $this->assertTrue( $provider->providerAllowsPropertyChange( 'foo' ) );
+               $this->assertEquals(
+                       \StatusValue::newGood( 'ignored' ),
+                       $provider->providerAllowsAuthenticationDataChange( $req )
+               );
+               $this->assertEquals(
+                       \StatusValue::newGood(),
+                       $provider->testForAccountCreation( $user, $user, [] )
+               );
+               $this->assertEquals(
+                       \StatusValue::newGood(),
+                       $provider->testUserForCreation( $user, AuthManager::AUTOCREATE_SOURCE_SESSION )
+               );
+               $this->assertEquals(
+                       \StatusValue::newGood(),
+                       $provider->testUserForCreation( $user, false )
+               );
+
+               $provider->providerChangeAuthenticationData( $req );
+               $provider->autoCreatedAccount( $user, AuthManager::AUTOCREATE_SOURCE_SESSION );
+
+               $res = AuthenticationResponse::newPass();
+               $provider->postAuthentication( $user, $res );
+               $provider->postAccountCreation( $user, $user, $res );
+       }
+
+       public function testProviderRevokeAccessForUser() {
+               $reqs = [];
+               for ( $i = 0; $i < 3; $i++ ) {
+                       $reqs[$i] = $this->createMock( AuthenticationRequest::class );
+                       $reqs[$i]->done = false;
+               }
+
+               $provider = $this->getMockBuilder( AbstractSecondaryAuthenticationProvider::class )
+                       ->setMethods( [ 'providerChangeAuthenticationData' ] )
+                       ->getMockForAbstractClass();
+               $provider->expects( $this->once() )->method( 'getAuthenticationRequests' )
+                       ->with(
+                               $this->identicalTo( AuthManager::ACTION_REMOVE ),
+                               $this->identicalTo( [ 'username' => 'UTSysop' ] )
+                       )
+                       ->will( $this->returnValue( $reqs ) );
+               $provider->expects( $this->exactly( 3 ) )->method( 'providerChangeAuthenticationData' )
+                       ->will( $this->returnCallback( function ( $req ) {
+                               $this->assertSame( 'UTSysop', $req->username );
+                               $this->assertFalse( $req->done );
+                               $req->done = true;
+                       } ) );
+
+               $provider->providerRevokeAccessForUser( 'UTSysop' );
+
+               foreach ( $reqs as $i => $req ) {
+                       $this->assertTrue( $req->done, "#$i" );
+               }
+       }
+}
diff --git a/tests/phpunit/unit/includes/auth/AuthenticationResponseTest.php b/tests/phpunit/unit/includes/auth/AuthenticationResponseTest.php
new file mode 100644 (file)
index 0000000..44b0631
--- /dev/null
@@ -0,0 +1,112 @@
+<?php
+
+namespace MediaWiki\Auth;
+
+/**
+ * @group AuthManager
+ * @covers \MediaWiki\Auth\AuthenticationResponse
+ */
+class AuthenticationResponseTest extends \MediaWikiUnitTestCase {
+       /**
+        * @dataProvider provideConstructors
+        * @param string $constructor
+        * @param array $args
+        * @param array|Exception $expect
+        */
+       public function testConstructors( $constructor, $args, $expect ) {
+               if ( is_array( $expect ) ) {
+                       $res = new AuthenticationResponse();
+                       $res->messageType = 'warning';
+                       foreach ( $expect as $field => $value ) {
+                               $res->$field = $value;
+                       }
+                       $ret = call_user_func_array( "MediaWiki\\Auth\\AuthenticationResponse::$constructor", $args );
+                       $this->assertEquals( $res, $ret );
+               } else {
+                       try {
+                               call_user_func_array( "MediaWiki\\Auth\\AuthenticationResponse::$constructor", $args );
+                               $this->fail( 'Expected exception not thrown' );
+                       } catch ( \Exception $ex ) {
+                               $this->assertEquals( $expect, $ex );
+                       }
+               }
+       }
+
+       public function provideConstructors() {
+               $req = $this->getMockForAbstractClass( AuthenticationRequest::class );
+               $msg = new \Message( 'mainpage' );
+
+               return [
+                       [ 'newPass', [], [
+                               'status' => AuthenticationResponse::PASS,
+                       ] ],
+                       [ 'newPass', [ 'name' ], [
+                               'status' => AuthenticationResponse::PASS,
+                               'username' => 'name',
+                       ] ],
+                       [ 'newPass', [ 'name', null ], [
+                               'status' => AuthenticationResponse::PASS,
+                               'username' => 'name',
+                       ] ],
+
+                       [ 'newFail', [ $msg ], [
+                               'status' => AuthenticationResponse::FAIL,
+                               'message' => $msg,
+                               'messageType' => 'error',
+                       ] ],
+
+                       [ 'newRestart', [ $msg ], [
+                               'status' => AuthenticationResponse::RESTART,
+                               'message' => $msg,
+                       ] ],
+
+                       [ 'newAbstain', [], [
+                               'status' => AuthenticationResponse::ABSTAIN,
+                       ] ],
+
+                       [ 'newUI', [ [ $req ], $msg ], [
+                               'status' => AuthenticationResponse::UI,
+                               'neededRequests' => [ $req ],
+                               'message' => $msg,
+                               'messageType' => 'warning',
+                       ] ],
+
+                       [ 'newUI', [ [ $req ], $msg, 'warning' ], [
+                               'status' => AuthenticationResponse::UI,
+                               'neededRequests' => [ $req ],
+                               'message' => $msg,
+                               'messageType' => 'warning',
+                       ] ],
+
+                       [ 'newUI', [ [ $req ], $msg, 'error' ], [
+                               'status' => AuthenticationResponse::UI,
+                               'neededRequests' => [ $req ],
+                               'message' => $msg,
+                               'messageType' => 'error',
+                       ] ],
+                       [ 'newUI', [ [], $msg ],
+                               new \InvalidArgumentException( '$reqs may not be empty' )
+                       ],
+
+                       [ 'newRedirect', [ [ $req ], 'http://example.org/redir' ], [
+                               'status' => AuthenticationResponse::REDIRECT,
+                               'neededRequests' => [ $req ],
+                               'redirectTarget' => 'http://example.org/redir',
+                       ] ],
+                       [
+                               'newRedirect',
+                               [ [ $req ], 'http://example.org/redir', [ 'foo' => 'bar' ] ],
+                               [
+                                       'status' => AuthenticationResponse::REDIRECT,
+                                       'neededRequests' => [ $req ],
+                                       'redirectTarget' => 'http://example.org/redir',
+                                       'redirectApiData' => [ 'foo' => 'bar' ],
+                               ]
+                       ],
+                       [ 'newRedirect', [ [], 'http://example.org/redir' ],
+                               new \InvalidArgumentException( '$reqs may not be empty' )
+                       ],
+               ];
+       }
+
+}
diff --git a/tests/phpunit/unit/includes/auth/ConfirmLinkSecondaryAuthenticationProviderTest.php b/tests/phpunit/unit/includes/auth/ConfirmLinkSecondaryAuthenticationProviderTest.php
new file mode 100644 (file)
index 0000000..7a4490b
--- /dev/null
@@ -0,0 +1,289 @@
+<?php
+
+namespace MediaWiki\Auth;
+
+use Wikimedia\TestingAccessWrapper;
+
+/**
+ * @group AuthManager
+ * @covers \MediaWiki\Auth\ConfirmLinkSecondaryAuthenticationProvider
+ */
+class ConfirmLinkSecondaryAuthenticationProviderTest extends \MediaWikiUnitTestCase {
+       /**
+        * @dataProvider provideGetAuthenticationRequests
+        * @param string $action
+        * @param array $response
+        */
+       public function testGetAuthenticationRequests( $action, $response ) {
+               $provider = new ConfirmLinkSecondaryAuthenticationProvider();
+
+               $this->assertEquals( $response, $provider->getAuthenticationRequests( $action, [] ) );
+       }
+
+       public static function provideGetAuthenticationRequests() {
+               return [
+                       [ AuthManager::ACTION_LOGIN, [] ],
+                       [ AuthManager::ACTION_CREATE, [] ],
+                       [ AuthManager::ACTION_LINK, [] ],
+                       [ AuthManager::ACTION_CHANGE, [] ],
+                       [ AuthManager::ACTION_REMOVE, [] ],
+               ];
+       }
+
+       public function testBeginSecondaryAuthentication() {
+               $user = \User::newFromName( 'UTSysop' );
+               $obj = new \stdClass;
+
+               $mock = $this->getMockBuilder( ConfirmLinkSecondaryAuthenticationProvider::class )
+                       ->setMethods( [ 'beginLinkAttempt', 'continueLinkAttempt' ] )
+                       ->getMock();
+               $mock->expects( $this->once() )->method( 'beginLinkAttempt' )
+                       ->with( $this->identicalTo( $user ), $this->identicalTo( 'AuthManager::authnState' ) )
+                       ->will( $this->returnValue( $obj ) );
+               $mock->expects( $this->never() )->method( 'continueLinkAttempt' );
+
+               $this->assertSame( $obj, $mock->beginSecondaryAuthentication( $user, [] ) );
+       }
+
+       public function testContinueSecondaryAuthentication() {
+               $user = \User::newFromName( 'UTSysop' );
+               $obj = new \stdClass;
+               $reqs = [ new \stdClass ];
+
+               $mock = $this->getMockBuilder( ConfirmLinkSecondaryAuthenticationProvider::class )
+                       ->setMethods( [ 'beginLinkAttempt', 'continueLinkAttempt' ] )
+                       ->getMock();
+               $mock->expects( $this->never() )->method( 'beginLinkAttempt' );
+               $mock->expects( $this->once() )->method( 'continueLinkAttempt' )
+                       ->with(
+                               $this->identicalTo( $user ),
+                               $this->identicalTo( 'AuthManager::authnState' ),
+                               $this->identicalTo( $reqs )
+                       )
+                       ->will( $this->returnValue( $obj ) );
+
+               $this->assertSame( $obj, $mock->continueSecondaryAuthentication( $user, $reqs ) );
+       }
+
+       public function testBeginSecondaryAccountCreation() {
+               $user = \User::newFromName( 'UTSysop' );
+               $obj = new \stdClass;
+
+               $mock = $this->getMockBuilder( ConfirmLinkSecondaryAuthenticationProvider::class )
+                       ->setMethods( [ 'beginLinkAttempt', 'continueLinkAttempt' ] )
+                       ->getMock();
+               $mock->expects( $this->once() )->method( 'beginLinkAttempt' )
+                       ->with( $this->identicalTo( $user ), $this->identicalTo( 'AuthManager::accountCreationState' ) )
+                       ->will( $this->returnValue( $obj ) );
+               $mock->expects( $this->never() )->method( 'continueLinkAttempt' );
+
+               $this->assertSame( $obj, $mock->beginSecondaryAccountCreation( $user, $user, [] ) );
+       }
+
+       public function testContinueSecondaryAccountCreation() {
+               $user = \User::newFromName( 'UTSysop' );
+               $obj = new \stdClass;
+               $reqs = [ new \stdClass ];
+
+               $mock = $this->getMockBuilder( ConfirmLinkSecondaryAuthenticationProvider::class )
+                       ->setMethods( [ 'beginLinkAttempt', 'continueLinkAttempt' ] )
+                       ->getMock();
+               $mock->expects( $this->never() )->method( 'beginLinkAttempt' );
+               $mock->expects( $this->once() )->method( 'continueLinkAttempt' )
+                       ->with(
+                               $this->identicalTo( $user ),
+                               $this->identicalTo( 'AuthManager::accountCreationState' ),
+                               $this->identicalTo( $reqs )
+                       )
+                       ->will( $this->returnValue( $obj ) );
+
+               $this->assertSame( $obj, $mock->continueSecondaryAccountCreation( $user, $user, $reqs ) );
+       }
+
+       /**
+        * Get requests for testing
+        * @return AuthenticationRequest[]
+        */
+       private function getLinkRequests() {
+               $reqs = [];
+
+               $mb = $this->getMockBuilder( AuthenticationRequest::class )
+                       ->setMethods( [ 'getUniqueId' ] );
+               for ( $i = 1; $i <= 3; $i++ ) {
+                       $req = $mb->getMockForAbstractClass();
+                       $req->expects( $this->any() )->method( 'getUniqueId' )
+                               ->will( $this->returnValue( "Request$i" ) );
+                       $req->id = $i - 1;
+                       $reqs[$req->getUniqueId()] = $req;
+               }
+
+               return $reqs;
+       }
+
+       public function testBeginLinkAttempt() {
+               $badReq = $this->getMockBuilder( AuthenticationRequest::class )
+                       ->setMethods( [ 'getUniqueId' ] )
+                       ->getMockForAbstractClass();
+               $badReq->expects( $this->any() )->method( 'getUniqueId' )
+                       ->will( $this->returnValue( "BadReq" ) );
+
+               $user = \User::newFromName( 'UTSysop' );
+               $provider = TestingAccessWrapper::newFromObject(
+                       new ConfirmLinkSecondaryAuthenticationProvider
+               );
+               $request = new \FauxRequest();
+               $manager = $this->getMockBuilder( AuthManager::class )
+                       ->setMethods( [ 'allowsAuthenticationDataChange' ] )
+                       ->setConstructorArgs( [ $request, \RequestContext::getMain()->getConfig() ] )
+                       ->getMock();
+               $manager->expects( $this->any() )->method( 'allowsAuthenticationDataChange' )
+                       ->will( $this->returnCallback( function ( $req ) {
+                               return $req->getUniqueId() !== 'BadReq'
+                                       ? \StatusValue::newGood()
+                                       : \StatusValue::newFatal( 'no' );
+                       } ) );
+               $provider->setManager( $manager );
+
+               $this->assertEquals(
+                       AuthenticationResponse::newAbstain(),
+                       $provider->beginLinkAttempt( $user, 'state' )
+               );
+
+               $request->getSession()->setSecret( 'state', [
+                       'maybeLink' => [],
+               ] );
+               $this->assertEquals(
+                       AuthenticationResponse::newAbstain(),
+                       $provider->beginLinkAttempt( $user, 'state' )
+               );
+
+               $reqs = $this->getLinkRequests();
+               $request->getSession()->setSecret( 'state', [
+                       'maybeLink' => $reqs + [ 'BadReq' => $badReq ]
+               ] );
+               $res = $provider->beginLinkAttempt( $user, 'state' );
+               $this->assertInstanceOf( AuthenticationResponse::class, $res );
+               $this->assertSame( AuthenticationResponse::UI, $res->status );
+               $this->assertSame( 'authprovider-confirmlink-message', $res->message->getKey() );
+               $this->assertCount( 1, $res->neededRequests );
+               $req = $res->neededRequests[0];
+               $this->assertInstanceOf( ConfirmLinkAuthenticationRequest::class, $req );
+               $expectReqs = $this->getLinkRequests();
+               foreach ( $expectReqs as $r ) {
+                       $r->action = AuthManager::ACTION_CHANGE;
+                       $r->username = $user->getName();
+               }
+               $this->assertEquals( $expectReqs, TestingAccessWrapper::newFromObject( $req )->linkRequests );
+       }
+
+       public function testContinueLinkAttempt() {
+               $user = \User::newFromName( 'UTSysop' );
+               $obj = new \stdClass;
+               $reqs = $this->getLinkRequests();
+
+               $done = [ false, false, false ];
+
+               // First, test the pass-through for not containing the ConfirmLinkAuthenticationRequest
+               $mock = $this->getMockBuilder( ConfirmLinkSecondaryAuthenticationProvider::class )
+                       ->setMethods( [ 'beginLinkAttempt' ] )
+                       ->getMock();
+               $mock->expects( $this->once() )->method( 'beginLinkAttempt' )
+                       ->with( $this->identicalTo( $user ), $this->identicalTo( 'state' ) )
+                       ->will( $this->returnValue( $obj ) );
+               $this->assertSame(
+                       $obj,
+                       TestingAccessWrapper::newFromObject( $mock )->continueLinkAttempt( $user, 'state', $reqs )
+               );
+
+               // Now test the actual functioning
+               $provider = $this->getMockBuilder( ConfirmLinkSecondaryAuthenticationProvider::class )
+                       ->setMethods( [
+                               'beginLinkAttempt', 'providerAllowsAuthenticationDataChange',
+                               'providerChangeAuthenticationData'
+                       ] )
+                       ->getMock();
+               $provider->expects( $this->never() )->method( 'beginLinkAttempt' );
+               $provider->expects( $this->any() )->method( 'providerAllowsAuthenticationDataChange' )
+                       ->will( $this->returnCallback( function ( $req ) use ( $reqs ) {
+                               return $req->getUniqueId() === 'Request3'
+                                       ? \StatusValue::newFatal( 'foo' ) : \StatusValue::newGood();
+                       } ) );
+               $provider->expects( $this->any() )->method( 'providerChangeAuthenticationData' )
+                       ->will( $this->returnCallback( function ( $req ) use ( &$done ) {
+                               $done[$req->id] = true;
+                       } ) );
+               $config = new \HashConfig( [
+                       'AuthManagerConfig' => [
+                               'preauth' => [],
+                               'primaryauth' => [],
+                               'secondaryauth' => [
+                                       [ 'factory' => function () use ( $provider ) {
+                                               return $provider;
+                                       } ],
+                               ],
+                       ],
+               ] );
+               $request = new \FauxRequest();
+               $manager = new AuthManager( $request, $config );
+               $provider->setManager( $manager );
+               $provider = TestingAccessWrapper::newFromObject( $provider );
+
+               $req = new ConfirmLinkAuthenticationRequest( $reqs );
+
+               $this->assertEquals(
+                       AuthenticationResponse::newAbstain(),
+                       $provider->continueLinkAttempt( $user, 'state', [ $req ] )
+               );
+
+               $request->getSession()->setSecret( 'state', [
+                       'maybeLink' => [],
+               ] );
+               $this->assertEquals(
+                       AuthenticationResponse::newAbstain(),
+                       $provider->continueLinkAttempt( $user, 'state', [ $req ] )
+               );
+
+               $request->getSession()->setSecret( 'state', [
+                       'maybeLink' => $reqs
+               ] );
+               $this->assertEquals(
+                       AuthenticationResponse::newPass(),
+                       $res = $provider->continueLinkAttempt( $user, 'state', [ $req ] )
+               );
+               $this->assertSame( [ false, false, false ], $done );
+
+               $request->getSession()->setSecret( 'state', [
+                       'maybeLink' => [ $reqs['Request2'] ],
+               ] );
+               $req->confirmedLinkIDs = [ 'Request1', 'Request2' ];
+               $res = $provider->continueLinkAttempt( $user, 'state', [ $req ] );
+               $this->assertEquals( AuthenticationResponse::newPass(), $res );
+               $this->assertSame( [ false, true, false ], $done );
+               $done = [ false, false, false ];
+
+               $request->getSession()->setSecret( 'state', [
+                       'maybeLink' => $reqs,
+               ] );
+               $req->confirmedLinkIDs = [ 'Request1', 'Request2' ];
+               $res = $provider->continueLinkAttempt( $user, 'state', [ $req ] );
+               $this->assertEquals( AuthenticationResponse::newPass(), $res );
+               $this->assertSame( [ true, true, false ], $done );
+               $done = [ false, false, false ];
+
+               $request->getSession()->setSecret( 'state', [
+                       'maybeLink' => $reqs,
+               ] );
+               $req->confirmedLinkIDs = [ 'Request1', 'Request3' ];
+               $res = $provider->continueLinkAttempt( $user, 'state', [ $req ] );
+               $this->assertEquals( AuthenticationResponse::UI, $res->status );
+               $this->assertCount( 1, $res->neededRequests );
+               $this->assertInstanceOf( ButtonAuthenticationRequest::class, $res->neededRequests[0] );
+               $this->assertSame( [ true, false, false ], $done );
+               $done = [ false, false, false ];
+
+               $res = $provider->continueLinkAttempt( $user, 'state', [ $res->neededRequests[0] ] );
+               $this->assertEquals( AuthenticationResponse::newPass(), $res );
+               $this->assertSame( [ false, false, false ], $done );
+       }
+
+}
diff --git a/tests/phpunit/unit/includes/auth/EmailNotificationSecondaryAuthenticationProviderTest.php b/tests/phpunit/unit/includes/auth/EmailNotificationSecondaryAuthenticationProviderTest.php
new file mode 100644 (file)
index 0000000..fcaf6bf
--- /dev/null
@@ -0,0 +1,139 @@
+<?php
+
+namespace MediaWiki\Auth;
+
+use Psr\Log\LoggerInterface;
+use Wikimedia\Rdbms\IDatabase;
+use Wikimedia\Rdbms\LoadBalancer;
+use Wikimedia\TestingAccessWrapper;
+
+/**
+ * @covers \MediaWiki\Auth\EmailNotificationSecondaryAuthenticationProvider
+ */
+class EmailNotificationSecondaryAuthenticationProviderTest extends \MediaWikiUnitTestCase {
+
+       protected function setUp() {
+               parent::setUp();
+
+               $lbMock = $this->createMock( LoadBalancer::class );
+               $dbMock = $this->getMockBuilder( IDatabase::class )
+                       ->disableOriginalConstructor()
+                       ->getMockForAbstractClass();
+
+               $lbMock->expects( $this->any() )
+                       ->method( 'getConnection' )
+                       ->willReturn( $dbMock );
+               $dbMock->expects( $this->any() )
+                       ->method( 'onTransactionCommitOrIdle' )
+                       ->willReturnCallback( function ( callable $callback ) {
+                               $callback();
+                       } );
+
+               $lbMockFactory = function () use ( $lbMock ): LoadBalancer {
+                       return $lbMock;
+               };
+
+               $this->overrideMwServices( [ 'DBLoadBalancer' => $lbMockFactory ] );
+       }
+
+       public function testConstructor() {
+               $config = new \HashConfig( [
+                       'EnableEmail' => true,
+                       'EmailAuthentication' => true,
+               ] );
+
+               $provider = new EmailNotificationSecondaryAuthenticationProvider();
+               $provider->setConfig( $config );
+               $providerPriv = TestingAccessWrapper::newFromObject( $provider );
+               $this->assertTrue( $providerPriv->sendConfirmationEmail );
+
+               $provider = new EmailNotificationSecondaryAuthenticationProvider( [
+                       'sendConfirmationEmail' => false,
+               ] );
+               $provider->setConfig( $config );
+               $providerPriv = TestingAccessWrapper::newFromObject( $provider );
+               $this->assertFalse( $providerPriv->sendConfirmationEmail );
+       }
+
+       /**
+        * @dataProvider provideGetAuthenticationRequests
+        * @param string $action
+        * @param AuthenticationRequest[] $expected
+        */
+       public function testGetAuthenticationRequests( $action, $expected ) {
+               $provider = new EmailNotificationSecondaryAuthenticationProvider( [
+                       'sendConfirmationEmail' => true,
+               ] );
+               $this->assertSame( $expected, $provider->getAuthenticationRequests( $action, [] ) );
+       }
+
+       public function provideGetAuthenticationRequests() {
+               return [
+                       [ AuthManager::ACTION_LOGIN, [] ],
+                       [ AuthManager::ACTION_CREATE, [] ],
+                       [ AuthManager::ACTION_LINK, [] ],
+                       [ AuthManager::ACTION_CHANGE, [] ],
+                       [ AuthManager::ACTION_REMOVE, [] ],
+               ];
+       }
+
+       public function testBeginSecondaryAuthentication() {
+               $provider = new EmailNotificationSecondaryAuthenticationProvider( [
+                       'sendConfirmationEmail' => true,
+               ] );
+               $this->assertEquals( AuthenticationResponse::newAbstain(),
+                       $provider->beginSecondaryAuthentication( \User::newFromName( 'Foo' ), [] ) );
+       }
+
+       public function testBeginSecondaryAccountCreation() {
+               $authManager = new AuthManager( new \FauxRequest(), new \HashConfig() );
+
+               $creator = $this->getMockBuilder( \User::class )->getMock();
+               $userWithoutEmail = $this->getMockBuilder( \User::class )->getMock();
+               $userWithoutEmail->expects( $this->any() )->method( 'getEmail' )->willReturn( '' );
+               $userWithoutEmail->expects( $this->any() )->method( 'getInstanceForUpdate' )->willReturnSelf();
+               $userWithoutEmail->expects( $this->never() )->method( 'sendConfirmationMail' );
+               $userWithEmailError = $this->getMockBuilder( \User::class )->getMock();
+               $userWithEmailError->expects( $this->any() )->method( 'getEmail' )->willReturn( 'foo@bar.baz' );
+               $userWithEmailError->expects( $this->any() )->method( 'getInstanceForUpdate' )->willReturnSelf();
+               $userWithEmailError->expects( $this->any() )->method( 'sendConfirmationMail' )
+                       ->willReturn( \Status::newFatal( 'fail' ) );
+               $userExpectsConfirmation = $this->getMockBuilder( \User::class )->getMock();
+               $userExpectsConfirmation->expects( $this->any() )->method( 'getEmail' )
+                       ->willReturn( 'foo@bar.baz' );
+               $userExpectsConfirmation->expects( $this->any() )->method( 'getInstanceForUpdate' )
+                       ->willReturnSelf();
+               $userExpectsConfirmation->expects( $this->once() )->method( 'sendConfirmationMail' )
+                       ->willReturn( \Status::newGood() );
+               $userNotExpectsConfirmation = $this->getMockBuilder( \User::class )->getMock();
+               $userNotExpectsConfirmation->expects( $this->any() )->method( 'getEmail' )
+                       ->willReturn( 'foo@bar.baz' );
+               $userNotExpectsConfirmation->expects( $this->any() )->method( 'getInstanceForUpdate' )
+                       ->willReturnSelf();
+               $userNotExpectsConfirmation->expects( $this->never() )->method( 'sendConfirmationMail' );
+
+               $provider = new EmailNotificationSecondaryAuthenticationProvider( [
+                       'sendConfirmationEmail' => false,
+               ] );
+               $provider->setManager( $authManager );
+               $provider->beginSecondaryAccountCreation( $userNotExpectsConfirmation, $creator, [] );
+
+               $provider = new EmailNotificationSecondaryAuthenticationProvider( [
+                       'sendConfirmationEmail' => true,
+               ] );
+               $provider->setManager( $authManager );
+               $provider->beginSecondaryAccountCreation( $userWithoutEmail, $creator, [] );
+               $provider->beginSecondaryAccountCreation( $userExpectsConfirmation, $creator, [] );
+
+               // test logging of email errors
+               $logger = $this->getMockForAbstractClass( LoggerInterface::class );
+               $logger->expects( $this->once() )->method( 'warning' );
+               $provider->setLogger( $logger );
+               $provider->beginSecondaryAccountCreation( $userWithEmailError, $creator, [] );
+
+               // test disable flag used by other providers
+               $authManager->setAuthenticationSessionData( 'no-email', true );
+               $provider->setManager( $authManager );
+               $provider->beginSecondaryAccountCreation( $userNotExpectsConfirmation, $creator, [] );
+       }
+}
diff --git a/tests/phpunit/unit/includes/changes/ChangesListFilterGroupTest.php b/tests/phpunit/unit/includes/changes/ChangesListFilterGroupTest.php
new file mode 100644 (file)
index 0000000..bd54d50
--- /dev/null
@@ -0,0 +1,79 @@
+<?php
+
+/**
+ * @covers ChangesListFilterGroup
+ */
+class ChangesListFilterGroupTest extends \MediaWikiUnitTestCase {
+       /**
+        * phpcs:disable Generic.Files.LineLength
+        * @expectedException MWException
+        * @expectedExceptionMessage Group names may not contain '_'.  Use the naming convention: 'camelCase'
+        * phpcs:enable
+        */
+       public function testReservedCharacter() {
+               new MockChangesListFilterGroup(
+                       [
+                               'type' => 'some_type',
+                               'name' => 'group_name',
+                               'priority' => 1,
+                               'filters' => [],
+                       ]
+               );
+       }
+
+       public function testAutoPriorities() {
+               $group = new MockChangesListFilterGroup(
+                       [
+                               'type' => 'some_type',
+                               'name' => 'groupName',
+                               'isFullCoverage' => true,
+                               'priority' => 1,
+                               'filters' => [
+                                       [ 'name' => 'hidefoo' ],
+                                       [ 'name' => 'hidebar' ],
+                                       [ 'name' => 'hidebaz' ],
+                               ],
+                       ]
+               );
+
+               $filters = $group->getFilters();
+               $this->assertEquals(
+                       [
+                               -2,
+                               -3,
+                               -4,
+                       ],
+                       array_map(
+                               function ( $f ) {
+                                       return $f->getPriority();
+                               },
+                               array_values( $filters )
+                       )
+               );
+       }
+
+       // Get without warnings
+       public function testGetFilter() {
+               $group = new MockChangesListFilterGroup(
+                       [
+                               'type' => 'some_type',
+                               'name' => 'groupName',
+                               'isFullCoverage' => true,
+                               'priority' => 1,
+                               'filters' => [
+                                       [ 'name' => 'foo' ],
+                               ],
+                       ]
+               );
+
+               $this->assertEquals(
+                       'foo',
+                       $group->getFilter( 'foo' )->getName()
+               );
+
+               $this->assertEquals(
+                       null,
+                       $group->getFilter( 'bar' )
+               );
+       }
+}
diff --git a/tests/phpunit/unit/includes/collation/CustomUppercaseCollationTest.php b/tests/phpunit/unit/includes/collation/CustomUppercaseCollationTest.php
new file mode 100644 (file)
index 0000000..0dfe59c
--- /dev/null
@@ -0,0 +1,68 @@
+<?php
+
+/**
+ * @covers CustomUppercaseCollation
+ */
+class CustomUppercaseCollationTest extends \MediaWikiUnitTestCase {
+
+       public function setUp() {
+               $this->collation = new CustomUppercaseCollation( [
+                       'D',
+                       'C',
+                       'Cs',
+                       'B'
+               ], Language::factory( 'en' ) );
+
+               parent::setUp();
+       }
+
+       /**
+        * @dataProvider providerOrder
+        */
+       public function testOrder( $first, $second, $msg ) {
+               $sortkey1 = $this->collation->getSortKey( $first );
+               $sortkey2 = $this->collation->getSortKey( $second );
+
+               $this->assertTrue( strcmp( $sortkey1, $sortkey2 ) < 0, $msg );
+       }
+
+       public function providerOrder() {
+               return [
+                       [ 'X', 'Z', 'Maintain order of unrearranged' ],
+                       [ 'D', 'C', 'Actually resorts' ],
+                       [ 'D', 'B', 'resort test 2' ],
+                       [ 'Adobe', 'Abode', 'not first letter' ],
+                       [ '💩 ', 'C', 'Test relocated to end' ],
+                       [ 'c', 'b', 'lowercase' ],
+                       [ 'x', 'z', 'lowercase original' ],
+                       [ 'Cz', 'Cs', 'digraphs' ],
+                       [ 'C50D', 'C100', 'Numbers' ]
+               ];
+       }
+
+       /**
+        * @dataProvider provideGetFirstLetter
+        */
+       public function testGetFirstLetter( $string, $first ) {
+               $this->assertSame( $this->collation->getFirstLetter( $string ), $first );
+       }
+
+       public function provideGetFirstLetter() {
+               return [
+                       [ 'Do', 'D' ],
+                       [ 'do', 'D' ],
+                       [ 'Ao', 'A' ],
+                       [ 'afdsa', 'A' ],
+                       [ "\u{F3000}Foo", 'D' ],
+                       [ "\u{F3001}Foo", 'C' ],
+                       [ "\u{F3002}Foo", 'Cs' ],
+                       [ "\u{F3003}Foo", 'B' ],
+                       [ "\u{F3004}Foo", "\u{F3004}" ],
+                       [ 'C', 'C' ],
+                       [ 'Cz', 'C' ],
+                       [ 'Cs', 'Cs' ],
+                       [ 'CS', 'Cs' ],
+                       [ 'cs', 'Cs' ],
+               ];
+       }
+}
diff --git a/tests/phpunit/unit/includes/composer/ComposerVersionNormalizerTest.php b/tests/phpunit/unit/includes/composer/ComposerVersionNormalizerTest.php
new file mode 100644 (file)
index 0000000..c5c0dc7
--- /dev/null
@@ -0,0 +1,163 @@
+<?php
+
+/**
+ * @covers ComposerVersionNormalizer
+ *
+ * @group ComposerHooks
+ *
+ * @author Jeroen De Dauw < jeroendedauw@gmail.com >
+ */
+class ComposerVersionNormalizerTest extends PHPUnit\Framework\TestCase {
+
+       use MediaWikiCoversValidator;
+       use PHPUnit4And6Compat;
+
+       /**
+        * @dataProvider nonStringProvider
+        */
+       public function testGivenNonString_normalizeThrowsInvalidArgumentException( $nonString ) {
+               $normalizer = new ComposerVersionNormalizer();
+
+               $this->setExpectedException( InvalidArgumentException::class );
+               $normalizer->normalizeSuffix( $nonString );
+       }
+
+       public function nonStringProvider() {
+               return [
+                       [ null ],
+                       [ 42 ],
+                       [ [] ],
+                       [ new stdClass() ],
+                       [ true ],
+               ];
+       }
+
+       /**
+        * @dataProvider simpleVersionProvider
+        */
+       public function testGivenSimpleVersion_normalizeSuffixReturnsAsIs( $simpleVersion ) {
+               $this->assertRemainsUnchanged( $simpleVersion );
+       }
+
+       protected function assertRemainsUnchanged( $version ) {
+               $normalizer = new ComposerVersionNormalizer();
+
+               $this->assertEquals(
+                       $version,
+                       $normalizer->normalizeSuffix( $version )
+               );
+       }
+
+       public function simpleVersionProvider() {
+               return [
+                       [ '1.22.0' ],
+                       [ '1.19.2' ],
+                       [ '1.19.2.0' ],
+                       [ '1.9' ],
+                       [ '123.321.456.654' ],
+               ];
+       }
+
+       /**
+        * @dataProvider complexVersionProvider
+        */
+       public function testGivenComplexVersionWithoutDash_normalizeSuffixAddsDash(
+               $withoutDash, $withDash
+       ) {
+               $normalizer = new ComposerVersionNormalizer();
+
+               $this->assertEquals(
+                       $withDash,
+                       $normalizer->normalizeSuffix( $withoutDash )
+               );
+       }
+
+       public function complexVersionProvider() {
+               return [
+                       [ '1.22.0alpha', '1.22.0-alpha' ],
+                       [ '1.22.0RC', '1.22.0-RC' ],
+                       [ '1.19beta', '1.19-beta' ],
+                       [ '1.9RC4', '1.9-RC4' ],
+                       [ '1.9.1.2RC4', '1.9.1.2-RC4' ],
+                       [ '1.9.1.2RC', '1.9.1.2-RC' ],
+                       [ '123.321.456.654RC9001', '123.321.456.654-RC9001' ],
+               ];
+       }
+
+       /**
+        * @dataProvider complexVersionProvider
+        */
+       public function testGivenComplexVersionWithDash_normalizeSuffixReturnsAsIs(
+               $withoutDash, $withDash
+       ) {
+               $this->assertRemainsUnchanged( $withDash );
+       }
+
+       /**
+        * @dataProvider fourLevelVersionsProvider
+        */
+       public function testGivenFourLevels_levelCountNormalizationDoesNothing( $version ) {
+               $normalizer = new ComposerVersionNormalizer();
+
+               $this->assertEquals(
+                       $version,
+                       $normalizer->normalizeLevelCount( $version )
+               );
+       }
+
+       public function fourLevelVersionsProvider() {
+               return [
+                       [ '1.22.0.0' ],
+                       [ '1.19.2.4' ],
+                       [ '1.19.2.0' ],
+                       [ '1.9.0.1' ],
+                       [ '123.321.456.654' ],
+                       [ '123.321.456.654RC4' ],
+                       [ '123.321.456.654-RC4' ],
+               ];
+       }
+
+       /**
+        * @dataProvider levelNormalizationProvider
+        */
+       public function testGivenFewerLevels_levelCountNormalizationEnsuresFourLevels(
+               $expected, $version
+       ) {
+               $normalizer = new ComposerVersionNormalizer();
+
+               $this->assertEquals(
+                       $expected,
+                       $normalizer->normalizeLevelCount( $version )
+               );
+       }
+
+       public function levelNormalizationProvider() {
+               return [
+                       [ '1.22.0.0', '1.22' ],
+                       [ '1.22.0.0', '1.22.0' ],
+                       [ '1.19.2.0', '1.19.2' ],
+                       [ '12345.0.0.0', '12345' ],
+                       [ '12345.0.0.0-RC4', '12345-RC4' ],
+                       [ '12345.0.0.0-alpha', '12345-alpha' ],
+               ];
+       }
+
+       /**
+        * @dataProvider invalidVersionProvider
+        */
+       public function testGivenInvalidVersion_normalizeSuffixReturnsAsIs( $invalidVersion ) {
+               $this->assertRemainsUnchanged( $invalidVersion );
+       }
+
+       public function invalidVersionProvider() {
+               return [
+                       [ '1.221-a' ],
+                       [ '1.221-' ],
+                       [ '1.22rc4a' ],
+                       [ 'a1.22rc' ],
+                       [ '.1.22rc' ],
+                       [ 'a' ],
+                       [ 'alpha42' ],
+               ];
+       }
+}
diff --git a/tests/phpunit/unit/includes/config/ConfigFactoryTest.php b/tests/phpunit/unit/includes/config/ConfigFactoryTest.php
new file mode 100644 (file)
index 0000000..a136018
--- /dev/null
@@ -0,0 +1,168 @@
+<?php
+
+use MediaWiki\MediaWikiServices;
+
+class ConfigFactoryTest extends \MediaWikiUnitTestCase {
+
+       /**
+        * @covers ConfigFactory::register
+        */
+       public function testRegister() {
+               $factory = new ConfigFactory();
+               $factory->register( 'unittest', 'GlobalVarConfig::newInstance' );
+               $this->assertInstanceOf( GlobalVarConfig::class, $factory->makeConfig( 'unittest' ) );
+       }
+
+       /**
+        * @covers ConfigFactory::register
+        */
+       public function testRegisterInvalid() {
+               $factory = new ConfigFactory();
+               $this->setExpectedException( InvalidArgumentException::class );
+               $factory->register( 'invalid', 'Invalid callback' );
+       }
+
+       /**
+        * @covers ConfigFactory::register
+        */
+       public function testRegisterInvalidInstance() {
+               $factory = new ConfigFactory();
+               $this->setExpectedException( InvalidArgumentException::class );
+               $factory->register( 'invalidInstance', new stdClass );
+       }
+
+       /**
+        * @covers ConfigFactory::register
+        */
+       public function testRegisterInstance() {
+               $config = GlobalVarConfig::newInstance();
+               $factory = new ConfigFactory();
+               $factory->register( 'unittest', $config );
+               $this->assertSame( $config, $factory->makeConfig( 'unittest' ) );
+       }
+
+       /**
+        * @covers ConfigFactory::register
+        */
+       public function testRegisterAgain() {
+               $factory = new ConfigFactory();
+               $factory->register( 'unittest', 'GlobalVarConfig::newInstance' );
+               $config1 = $factory->makeConfig( 'unittest' );
+
+               $factory->register( 'unittest', 'GlobalVarConfig::newInstance' );
+               $config2 = $factory->makeConfig( 'unittest' );
+
+               $this->assertNotSame( $config1, $config2 );
+       }
+
+       /**
+        * @covers ConfigFactory::salvage
+        */
+       public function testSalvage() {
+               $oldFactory = new ConfigFactory();
+               $oldFactory->register( 'foo', 'GlobalVarConfig::newInstance' );
+               $oldFactory->register( 'bar', 'GlobalVarConfig::newInstance' );
+               $oldFactory->register( 'quux', 'GlobalVarConfig::newInstance' );
+
+               // instantiate two of the three defined configurations
+               $foo = $oldFactory->makeConfig( 'foo' );
+               $bar = $oldFactory->makeConfig( 'bar' );
+               $quux = $oldFactory->makeConfig( 'quux' );
+
+               // define new config instance
+               $newFactory = new ConfigFactory();
+               $newFactory->register( 'foo', 'GlobalVarConfig::newInstance' );
+               $newFactory->register( 'bar', function () {
+                       return new HashConfig();
+               } );
+
+               // "foo" and "quux" are defined in the old and the new factory.
+               // The old factory has instances for "foo" and "bar", but not "quux".
+               $newFactory->salvage( $oldFactory );
+
+               $newFoo = $newFactory->makeConfig( 'foo' );
+               $this->assertSame( $foo, $newFoo, 'existing instance should be salvaged' );
+
+               $newBar = $newFactory->makeConfig( 'bar' );
+               $this->assertNotSame( $bar, $newBar, 'don\'t salvage if callbacks differ' );
+
+               // the new factory doesn't have quux defined, so the quux instance should not be salvaged
+               $this->setExpectedException( ConfigException::class );
+               $newFactory->makeConfig( 'quux' );
+       }
+
+       /**
+        * @covers ConfigFactory::getConfigNames
+        */
+       public function testGetConfigNames() {
+               $factory = new ConfigFactory();
+               $factory->register( 'foo', 'GlobalVarConfig::newInstance' );
+               $factory->register( 'bar', new HashConfig() );
+
+               $this->assertEquals( [ 'foo', 'bar' ], $factory->getConfigNames() );
+       }
+
+       /**
+        * @covers ConfigFactory::makeConfig
+        */
+       public function testMakeConfigWithCallback() {
+               $factory = new ConfigFactory();
+               $factory->register( 'unittest', 'GlobalVarConfig::newInstance' );
+
+               $conf = $factory->makeConfig( 'unittest' );
+               $this->assertInstanceOf( Config::class, $conf );
+               $this->assertSame( $conf, $factory->makeConfig( 'unittest' ) );
+       }
+
+       /**
+        * @covers ConfigFactory::makeConfig
+        */
+       public function testMakeConfigWithObject() {
+               $factory = new ConfigFactory();
+               $conf = new HashConfig();
+               $factory->register( 'test', $conf );
+               $this->assertSame( $conf, $factory->makeConfig( 'test' ) );
+       }
+
+       /**
+        * @covers ConfigFactory::makeConfig
+        */
+       public function testMakeConfigFallback() {
+               $factory = new ConfigFactory();
+               $factory->register( '*', 'GlobalVarConfig::newInstance' );
+               $conf = $factory->makeConfig( 'unittest' );
+               $this->assertInstanceOf( Config::class, $conf );
+       }
+
+       /**
+        * @covers ConfigFactory::makeConfig
+        */
+       public function testMakeConfigWithNoBuilders() {
+               $factory = new ConfigFactory();
+               $this->setExpectedException( ConfigException::class );
+               $factory->makeConfig( 'nobuilderregistered' );
+       }
+
+       /**
+        * @covers ConfigFactory::makeConfig
+        */
+       public function testMakeConfigWithInvalidCallback() {
+               $factory = new ConfigFactory();
+               $factory->register( 'unittest', function () {
+                       return true; // Not a Config object
+               } );
+               $this->setExpectedException( UnexpectedValueException::class );
+               $factory->makeConfig( 'unittest' );
+       }
+
+       /**
+        * @covers ConfigFactory::getDefaultInstance
+        */
+       public function testGetDefaultInstance() {
+               // NOTE: the global config factory returned here has been overwritten
+               // for operation in test mode. It may not reflect LocalSettings.
+               $factory = MediaWikiServices::getInstance()->getConfigFactory();
+               $this->assertInstanceOf( Config::class, $factory->makeConfig( 'main' ) );
+       }
+
+}
diff --git a/tests/phpunit/unit/includes/config/EtcdConfigTest.php b/tests/phpunit/unit/includes/config/EtcdConfigTest.php
new file mode 100644 (file)
index 0000000..3eecf82
--- /dev/null
@@ -0,0 +1,621 @@
+<?php
+
+use Wikimedia\TestingAccessWrapper;
+
+class EtcdConfigTest extends PHPUnit\Framework\TestCase {
+
+       use MediaWikiCoversValidator;
+       use PHPUnit4And6Compat;
+
+       private function createConfigMock( array $options = [] ) {
+               return $this->getMockBuilder( EtcdConfig::class )
+                       ->setConstructorArgs( [ $options + [
+                               'host' => 'etcd-tcp.example.net',
+                               'directory' => '/',
+                               'timeout' => 0.1,
+                       ] ] )
+                       ->setMethods( [ 'fetchAllFromEtcd' ] )
+                       ->getMock();
+       }
+
+       private static function createEtcdResponse( array $response ) {
+               $baseResponse = [
+                       'config' => null,
+                       'error' => null,
+                       'retry' => false,
+                       'modifiedIndex' => 0,
+               ];
+               return array_merge( $baseResponse, $response );
+       }
+
+       private function createSimpleConfigMock( array $config, $index = 0 ) {
+               $mock = $this->createConfigMock();
+               $mock->expects( $this->once() )->method( 'fetchAllFromEtcd' )
+                       ->willReturn( self::createEtcdResponse( [
+                               'config' => $config,
+                               'modifiedIndex' => $index,
+                       ] ) );
+               return $mock;
+       }
+
+       /**
+        * @covers EtcdConfig::has
+        */
+       public function testHasKnown() {
+               $config = $this->createSimpleConfigMock( [
+                       'known' => 'value'
+               ] );
+               $this->assertSame( true, $config->has( 'known' ) );
+       }
+
+       /**
+        * @covers EtcdConfig::__construct
+        * @covers EtcdConfig::get
+        */
+       public function testGetKnown() {
+               $config = $this->createSimpleConfigMock( [
+                       'known' => 'value'
+               ] );
+               $this->assertSame( 'value', $config->get( 'known' ) );
+       }
+
+       /**
+        * @covers EtcdConfig::has
+        */
+       public function testHasUnknown() {
+               $config = $this->createSimpleConfigMock( [
+                       'known' => 'value'
+               ] );
+               $this->assertSame( false, $config->has( 'unknown' ) );
+       }
+
+       /**
+        * @covers EtcdConfig::get
+        */
+       public function testGetUnknown() {
+               $config = $this->createSimpleConfigMock( [
+                       'known' => 'value'
+               ] );
+               $this->setExpectedException( ConfigException::class );
+               $config->get( 'unknown' );
+       }
+
+       /**
+        * @covers EtcdConfig::getModifiedIndex
+        */
+       public function testGetModifiedIndex() {
+               $config = $this->createSimpleConfigMock(
+                       [ 'some' => 'value' ],
+                       123
+               );
+               $this->assertSame( 123, $config->getModifiedIndex() );
+       }
+
+       /**
+        * @covers EtcdConfig::__construct
+        */
+       public function testConstructCacheObj() {
+               $cache = $this->getMockBuilder( HashBagOStuff::class )
+                       ->setMethods( [ 'get' ] )
+                       ->getMock();
+               $cache->expects( $this->once() )->method( 'get' )
+                       ->willReturn( [
+                               'config' => [ 'known' => 'from-cache' ],
+                               'expires' => INF,
+                               'modifiedIndex' => 123
+                       ] );
+               $config = $this->createConfigMock( [ 'cache' => $cache ] );
+
+               $this->assertSame( 'from-cache', $config->get( 'known' ) );
+       }
+
+       /**
+        * @covers EtcdConfig::__construct
+        */
+       public function testConstructCacheSpec() {
+               $config = $this->createConfigMock( [ 'cache' => [
+                       'class' => HashBagOStuff::class
+               ] ] );
+               $config->expects( $this->once() )->method( 'fetchAllFromEtcd' )
+                       ->willReturn( self::createEtcdResponse(
+                               [ 'config' => [ 'known' => 'from-fetch' ], ] ) );
+
+               $this->assertSame( 'from-fetch', $config->get( 'known' ) );
+       }
+
+       /**
+        * Test matrix
+        *
+        * - [x] Cache miss
+        *       Result: Fetched value
+        *       > cache miss | gets lock | backend succeeds
+        *
+        * - [x] Cache miss with backend error
+        *       Result: ConfigException
+        *       > cache miss | gets lock | backend error (no retry)
+        *
+        * - [x] Cache hit after retry
+        *       Result: Cached value (populated by process holding lock)
+        *       > cache miss | no lock | cache retry
+        *
+        * - [x] Cache hit
+        *       Result: Cached value
+        *       > cache hit
+        *
+        * - [x] Process cache hit
+        *       Result: Cached value
+        *       > process cache hit
+        *
+        * - [x] Cache expired
+        *       Result: Fetched value
+        *       > cache expired | gets lock | backend succeeds
+        *
+        * - [x] Cache expired with backend failure
+        *       Result: Cached value (stale)
+        *       > cache expired | gets lock | backend fails (allows retry)
+        *
+        * - [x] Cache expired and no lock
+        *       Result: Cached value (stale)
+        *       > cache expired | no lock
+        *
+        * Other notable scenarios:
+        *
+        * - [ ] Cache miss with backend retry
+        *       Result: Fetched value
+        *       > cache expired | gets lock | backend failure (allows retry)
+        */
+
+       /**
+        * @covers EtcdConfig::load
+        */
+       public function testLoadCacheMiss() {
+               // Create cache mock
+               $cache = $this->getMockBuilder( HashBagOStuff::class )
+                       ->setMethods( [ 'get', 'lock' ] )
+                       ->getMock();
+               // .. misses cache
+               $cache->expects( $this->once() )->method( 'get' )
+                       ->willReturn( false );
+               // .. gets lock
+               $cache->expects( $this->once() )->method( 'lock' )
+                       ->willReturn( true );
+
+               // Create config mock
+               $mock = $this->createConfigMock( [
+                       'cache' => $cache,
+               ] );
+               $mock->expects( $this->once() )->method( 'fetchAllFromEtcd' )
+                       ->willReturn(
+                               self::createEtcdResponse( [ 'config' => [ 'known' => 'from-fetch' ] ] ) );
+
+               $this->assertSame( 'from-fetch', $mock->get( 'known' ) );
+       }
+
+       /**
+        * @covers EtcdConfig::load
+        */
+       public function testLoadCacheMissBackendError() {
+               // Create cache mock
+               $cache = $this->getMockBuilder( HashBagOStuff::class )
+                       ->setMethods( [ 'get', 'lock' ] )
+                       ->getMock();
+               // .. misses cache
+               $cache->expects( $this->once() )->method( 'get' )
+                       ->willReturn( false );
+               // .. gets lock
+               $cache->expects( $this->once() )->method( 'lock' )
+                       ->willReturn( true );
+
+               // Create config mock
+               $mock = $this->createConfigMock( [
+                       'cache' => $cache,
+               ] );
+               $mock->expects( $this->once() )->method( 'fetchAllFromEtcd' )
+                       ->willReturn( self::createEtcdResponse( [ 'error' => 'Fake error', ] ) );
+
+               $this->setExpectedException( ConfigException::class );
+               $mock->get( 'key' );
+       }
+
+       /**
+        * @covers EtcdConfig::load
+        */
+       public function testLoadCacheMissWithoutLock() {
+               // Create cache mock
+               $cache = $this->getMockBuilder( HashBagOStuff::class )
+                       ->setMethods( [ 'get', 'lock' ] )
+                       ->getMock();
+               $cache->expects( $this->exactly( 2 ) )->method( 'get' )
+                       ->will( $this->onConsecutiveCalls(
+                               // .. misses cache first time
+                               false,
+                               // .. hits cache on retry
+                               [
+                                       'config' => [ 'known' => 'from-cache' ],
+                                       'expires' => INF,
+                                       'modifiedIndex' => 123
+                               ]
+                       ) );
+               // .. misses lock
+               $cache->expects( $this->once() )->method( 'lock' )
+                       ->willReturn( false );
+
+               // Create config mock
+               $mock = $this->createConfigMock( [
+                       'cache' => $cache,
+               ] );
+               $mock->expects( $this->never() )->method( 'fetchAllFromEtcd' );
+
+               $this->assertSame( 'from-cache', $mock->get( 'known' ) );
+       }
+
+       /**
+        * @covers EtcdConfig::load
+        */
+       public function testLoadCacheHit() {
+               // Create cache mock
+               $cache = $this->getMockBuilder( HashBagOStuff::class )
+                       ->setMethods( [ 'get', 'lock' ] )
+                       ->getMock();
+               $cache->expects( $this->once() )->method( 'get' )
+                       // .. hits cache
+                       ->willReturn( [
+                               'config' => [ 'known' => 'from-cache' ],
+                               'expires' => INF,
+                               'modifiedIndex' => 0,
+                       ] );
+               $cache->expects( $this->never() )->method( 'lock' );
+
+               // Create config mock
+               $mock = $this->createConfigMock( [
+                       'cache' => $cache,
+               ] );
+               $mock->expects( $this->never() )->method( 'fetchAllFromEtcd' );
+
+               $this->assertSame( 'from-cache', $mock->get( 'known' ) );
+       }
+
+       /**
+        * @covers EtcdConfig::load
+        */
+       public function testLoadProcessCacheHit() {
+               // Create cache mock
+               $cache = $this->getMockBuilder( HashBagOStuff::class )
+                       ->setMethods( [ 'get', 'lock' ] )
+                       ->getMock();
+               $cache->expects( $this->once() )->method( 'get' )
+                       // .. hits cache
+                       ->willReturn( [
+                               'config' => [ 'known' => 'from-cache' ],
+                               'expires' => INF,
+                               'modifiedIndex' => 0,
+                       ] );
+               $cache->expects( $this->never() )->method( 'lock' );
+
+               // Create config mock
+               $mock = $this->createConfigMock( [
+                       'cache' => $cache,
+               ] );
+               $mock->expects( $this->never() )->method( 'fetchAllFromEtcd' );
+
+               $this->assertSame( 'from-cache', $mock->get( 'known' ), 'Cache hit' );
+               $this->assertSame( 'from-cache', $mock->get( 'known' ), 'Process cache hit' );
+       }
+
+       /**
+        * @covers EtcdConfig::load
+        */
+       public function testLoadCacheExpiredLockFetchSucceeded() {
+               // Create cache mock
+               $cache = $this->getMockBuilder( HashBagOStuff::class )
+                       ->setMethods( [ 'get', 'lock' ] )
+                       ->getMock();
+               $cache->expects( $this->once() )->method( 'get' )->willReturn(
+                       // .. stale cache
+                       [
+                               'config' => [ 'known' => 'from-cache-expired' ],
+                               'expires' => -INF,
+                               'modifiedIndex' => 0,
+                       ]
+               );
+               // .. gets lock
+               $cache->expects( $this->once() )->method( 'lock' )
+                       ->willReturn( true );
+
+               // Create config mock
+               $mock = $this->createConfigMock( [
+                       'cache' => $cache,
+               ] );
+               $mock->expects( $this->once() )->method( 'fetchAllFromEtcd' )
+                       ->willReturn( self::createEtcdResponse( [ 'config' => [ 'known' => 'from-fetch' ] ] ) );
+
+               $this->assertSame( 'from-fetch', $mock->get( 'known' ) );
+       }
+
+       /**
+        * @covers EtcdConfig::load
+        */
+       public function testLoadCacheExpiredLockFetchFails() {
+               // Create cache mock
+               $cache = $this->getMockBuilder( HashBagOStuff::class )
+                       ->setMethods( [ 'get', 'lock' ] )
+                       ->getMock();
+               $cache->expects( $this->once() )->method( 'get' )->willReturn(
+                       // .. stale cache
+                       [
+                               'config' => [ 'known' => 'from-cache-expired' ],
+                               'expires' => -INF,
+                               'modifiedIndex' => 0,
+                       ]
+               );
+               // .. gets lock
+               $cache->expects( $this->once() )->method( 'lock' )
+                       ->willReturn( true );
+
+               // Create config mock
+               $mock = $this->createConfigMock( [
+                       'cache' => $cache,
+               ] );
+               $mock->expects( $this->once() )->method( 'fetchAllFromEtcd' )
+                       ->willReturn( self::createEtcdResponse( [ 'error' => 'Fake failure', 'retry' => true ] ) );
+
+               $this->assertSame( 'from-cache-expired', $mock->get( 'known' ) );
+       }
+
+       /**
+        * @covers EtcdConfig::load
+        */
+       public function testLoadCacheExpiredNoLock() {
+               // Create cache mock
+               $cache = $this->getMockBuilder( HashBagOStuff::class )
+                       ->setMethods( [ 'get', 'lock' ] )
+                       ->getMock();
+               $cache->expects( $this->once() )->method( 'get' )
+                       // .. hits cache (expired value)
+                       ->willReturn( [
+                               'config' => [ 'known' => 'from-cache-expired' ],
+                               'expires' => -INF,
+                               'modifiedIndex' => 0,
+                       ] );
+               // .. misses lock
+               $cache->expects( $this->once() )->method( 'lock' )
+                       ->willReturn( false );
+
+               // Create config mock
+               $mock = $this->createConfigMock( [
+                       'cache' => $cache,
+               ] );
+               $mock->expects( $this->never() )->method( 'fetchAllFromEtcd' );
+
+               $this->assertSame( 'from-cache-expired', $mock->get( 'known' ) );
+       }
+
+       public static function provideFetchFromServer() {
+               return [
+                       '200 OK - Success' => [
+                               'http' => [
+                                       'code' => 200,
+                                       'reason' => 'OK',
+                                       'headers' => [],
+                                       'body' => json_encode( [ 'node' => [ 'nodes' => [
+                                               [
+                                                       'key' => '/example/foo',
+                                                       'value' => json_encode( [ 'val' => true ] ),
+                                                       'modifiedIndex' => 123
+                                               ],
+                                       ] ] ] ),
+                                       'error' => '',
+                               ],
+                               'expect' => self::createEtcdResponse( [
+                                       'config' => [ 'foo' => true ], // data
+                                       'modifiedIndex' => 123
+                               ] ),
+                       ],
+                       '200 OK - Empty dir' => [
+                               'http' => [
+                                       'code' => 200,
+                                       'reason' => 'OK',
+                                       'headers' => [],
+                                       'body' => json_encode( [ 'node' => [ 'nodes' => [
+                                               [
+                                                       'key' => '/example/foo',
+                                                       'value' => json_encode( [ 'val' => true ] ),
+                                                       'modifiedIndex' => 123
+                                               ],
+                                               [
+                                                       'key' => '/example/sub',
+                                                       'dir' => true,
+                                                       'modifiedIndex' => 234,
+                                                       'nodes' => [],
+                                               ],
+                                               [
+                                                       'key' => '/example/bar',
+                                                       'value' => json_encode( [ 'val' => false ] ),
+                                                       'modifiedIndex' => 125
+                                               ],
+                                       ] ] ] ),
+                                       'error' => '',
+                               ],
+                               'expect' => self::createEtcdResponse( [
+                                       'config' => [ 'foo' => true, 'bar' => false ], // data
+                                       'modifiedIndex' => 125 // largest modified index
+                               ] ),
+                       ],
+                       '200 OK - Recursive' => [
+                               'http' => [
+                                       'code' => 200,
+                                       'reason' => 'OK',
+                                       'headers' => [],
+                                       'body' => json_encode( [ 'node' => [ 'nodes' => [
+                                               [
+                                                       'key' => '/example/a',
+                                                       'dir' => true,
+                                                       'modifiedIndex' => 124,
+                                                       'nodes' => [
+                                                               [
+                                                                       'key' => 'b',
+                                                                       'value' => json_encode( [ 'val' => true ] ),
+                                                                       'modifiedIndex' => 123,
+
+                                                               ],
+                                                               [
+                                                                       'key' => 'c',
+                                                                       'value' => json_encode( [ 'val' => false ] ),
+                                                                       'modifiedIndex' => 123,
+                                                               ],
+                                                       ],
+                                               ],
+                                       ] ] ] ),
+                                       'error' => '',
+                               ],
+                               'expect' => self::createEtcdResponse( [
+                                       'config' => [ 'a/b' => true, 'a/c' => false ], // data
+                                       'modifiedIndex' => 123 // largest modified index
+                               ] ),
+                       ],
+                       '200 OK - Missing nodes at second level' => [
+                               'http' => [
+                                       'code' => 200,
+                                       'reason' => 'OK',
+                                       'headers' => [],
+                                       'body' => json_encode( [ 'node' => [ 'nodes' => [
+                                               [
+                                                       'key' => '/example/a',
+                                                       'dir' => true,
+                                                       'modifiedIndex' => 0,
+                                               ],
+                                       ] ] ] ),
+                                       'error' => '',
+                               ],
+                               'expect' => self::createEtcdResponse( [
+                                       'error' => "Unexpected JSON response in dir 'a'; missing 'nodes' list.",
+                               ] ),
+                       ],
+                       '200 OK - Directory with non-array "nodes" key' => [
+                               'http' => [
+                                       'code' => 200,
+                                       'reason' => 'OK',
+                                       'headers' => [],
+                                       'body' => json_encode( [ 'node' => [ 'nodes' => [
+                                               [
+                                                       'key' => '/example/a',
+                                                       'dir' => true,
+                                                       'nodes' => 'not an array'
+                                               ],
+                                       ] ] ] ),
+                                       'error' => '',
+                               ],
+                               'expect' => self::createEtcdResponse( [
+                                       'error' => "Unexpected JSON response in dir 'a'; 'nodes' is not an array.",
+                               ] ),
+                       ],
+                       '200 OK - Correctly encoded garbage response' => [
+                               'http' => [
+                                       'code' => 200,
+                                       'reason' => 'OK',
+                                       'headers' => [],
+                                       'body' => json_encode( [ 'foo' => 'bar' ] ),
+                                       'error' => '',
+                               ],
+                               'expect' => self::createEtcdResponse( [
+                                       'error' => "Unexpected JSON response: Missing or invalid node at top level.",
+                               ] ),
+                       ],
+                       '200 OK - Bad value' => [
+                               'http' => [
+                                       'code' => 200,
+                                       'reason' => 'OK',
+                                       'headers' => [],
+                                       'body' => json_encode( [ 'node' => [ 'nodes' => [
+                                               [
+                                                       'key' => '/example/foo',
+                                                       'value' => ';"broken{value',
+                                                       'modifiedIndex' => 123,
+                                               ]
+                                       ] ] ] ),
+                                       'error' => '',
+                               ],
+                               'expect' => self::createEtcdResponse( [
+                                       'error' => "Failed to parse value for 'foo'.",
+                               ] ),
+                       ],
+                       '200 OK - Empty node list' => [
+                               'http' => [
+                                       'code' => 200,
+                                       'reason' => 'OK',
+                                       'headers' => [],
+                                       'body' => '{"node":{"nodes":[], "modifiedIndex": 12 }}',
+                                       'error' => '',
+                               ],
+                               'expect' => self::createEtcdResponse( [
+                                       'config' => [], // data
+                               ] ),
+                       ],
+                       '200 OK - Invalid JSON' => [
+                               'http' => [
+                                       'code' => 200,
+                                       'reason' => 'OK',
+                                       'headers' => [ 'content-length' => 0 ],
+                                       'body' => '',
+                                       'error' => '(curl error: no status set)',
+                               ],
+                               'expect' => self::createEtcdResponse( [
+                                       'error' => "Error unserializing JSON response.",
+                               ] ),
+                       ],
+                       '404 Not Found' => [
+                               'http' => [
+                                       'code' => 404,
+                                       'reason' => 'Not Found',
+                                       'headers' => [ 'content-length' => 0 ],
+                                       'body' => '',
+                                       'error' => '',
+                               ],
+                               'expect' => self::createEtcdResponse( [
+                                       'error' => 'HTTP 404 (Not Found)',
+                               ] ),
+                       ],
+                       '400 Bad Request - custom error' => [
+                               'http' => [
+                                       'code' => 400,
+                                       'reason' => 'Bad Request',
+                                       'headers' => [ 'content-length' => 0 ],
+                                       'body' => '',
+                                       'error' => 'No good reason',
+                               ],
+                               'expect' => self::createEtcdResponse( [
+                                       'error' => 'No good reason',
+                                       'retry' => true, // retry
+                               ] ),
+                       ],
+               ];
+       }
+
+       /**
+        * @covers EtcdConfig::fetchAllFromEtcdServer
+        * @covers EtcdConfig::unserialize
+        * @covers EtcdConfig::parseResponse
+        * @covers EtcdConfig::parseDirectory
+        * @covers EtcdConfigParseError
+        * @dataProvider provideFetchFromServer
+        */
+       public function testFetchFromServer( array $httpResponse, array $expected ) {
+               $http = $this->getMockBuilder( MultiHttpClient::class )
+                       ->disableOriginalConstructor()
+                       ->getMock();
+               $http->expects( $this->once() )->method( 'run' )
+                       ->willReturn( array_values( $httpResponse ) );
+
+               $conf = $this->getMockBuilder( EtcdConfig::class )
+                       ->disableOriginalConstructor()
+                       ->getMock();
+               // Access for protected member and method
+               $conf = TestingAccessWrapper::newFromObject( $conf );
+               $conf->http = $http;
+
+               $this->assertSame(
+                       $expected,
+                       $conf->fetchAllFromEtcdServer( 'etcd-tcp.example.net' )
+               );
+       }
+}
diff --git a/tests/phpunit/unit/includes/config/HashConfigTest.php b/tests/phpunit/unit/includes/config/HashConfigTest.php
new file mode 100644 (file)
index 0000000..d46ee09
--- /dev/null
@@ -0,0 +1,63 @@
+<?php
+
+class HashConfigTest extends \MediaWikiUnitTestCase {
+
+       /**
+        * @covers HashConfig::newInstance
+        */
+       public function testNewInstance() {
+               $conf = HashConfig::newInstance();
+               $this->assertInstanceOf( HashConfig::class, $conf );
+       }
+
+       /**
+        * @covers HashConfig::__construct
+        */
+       public function testConstructor() {
+               $conf = new HashConfig();
+               $this->assertInstanceOf( HashConfig::class, $conf );
+
+               // Test passing arguments to the constructor
+               $conf2 = new HashConfig( [
+                       'one' => '1',
+               ] );
+               $this->assertEquals( '1', $conf2->get( 'one' ) );
+       }
+
+       /**
+        * @covers HashConfig::get
+        */
+       public function testGet() {
+               $conf = new HashConfig( [
+                       'one' => '1',
+               ] );
+               $this->assertEquals( '1', $conf->get( 'one' ) );
+               $this->setExpectedException( ConfigException::class, 'HashConfig::get: undefined option' );
+               $conf->get( 'two' );
+       }
+
+       /**
+        * @covers HashConfig::has
+        */
+       public function testHas() {
+               $conf = new HashConfig( [
+                       'one' => '1',
+               ] );
+               $this->assertTrue( $conf->has( 'one' ) );
+               $this->assertFalse( $conf->has( 'two' ) );
+       }
+
+       /**
+        * @covers HashConfig::set
+        */
+       public function testSet() {
+               $conf = new HashConfig( [
+                       'one' => '1',
+               ] );
+               $conf->set( 'two', '2' );
+               $this->assertEquals( '2', $conf->get( 'two' ) );
+               // Check that set overwrites
+               $conf->set( 'one', '3' );
+               $this->assertEquals( '3', $conf->get( 'one' ) );
+       }
+}
diff --git a/tests/phpunit/unit/includes/config/MultiConfigTest.php b/tests/phpunit/unit/includes/config/MultiConfigTest.php
new file mode 100644 (file)
index 0000000..4351151
--- /dev/null
@@ -0,0 +1,39 @@
+<?php
+
+class MultiConfigTest extends \MediaWikiUnitTestCase {
+
+       /**
+        * Tests that settings are fetched in the right order
+        *
+        * @covers MultiConfig::__construct
+        * @covers MultiConfig::get
+        */
+       public function testGet() {
+               $multi = new MultiConfig( [
+                       new HashConfig( [ 'foo' => 'bar' ] ),
+                       new HashConfig( [ 'foo' => 'baz', 'bar' => 'foo' ] ),
+                       new HashConfig( [ 'bar' => 'baz' ] ),
+               ] );
+
+               $this->assertEquals( 'bar', $multi->get( 'foo' ) );
+               $this->assertEquals( 'foo', $multi->get( 'bar' ) );
+               $this->setExpectedException( ConfigException::class, 'MultiConfig::get: undefined option:' );
+               $multi->get( 'notset' );
+       }
+
+       /**
+        * @covers MultiConfig::has
+        */
+       public function testHas() {
+               $conf = new MultiConfig( [
+                       new HashConfig( [ 'foo' => 'foo' ] ),
+                       new HashConfig( [ 'something' => 'bleh' ] ),
+                       new HashConfig( [ 'meh' => 'eh' ] ),
+               ] );
+
+               $this->assertTrue( $conf->has( 'foo' ) );
+               $this->assertTrue( $conf->has( 'something' ) );
+               $this->assertTrue( $conf->has( 'meh' ) );
+               $this->assertFalse( $conf->has( 'what' ) );
+       }
+}
diff --git a/tests/phpunit/unit/includes/config/ServiceOptionsTest.php b/tests/phpunit/unit/includes/config/ServiceOptionsTest.php
new file mode 100644 (file)
index 0000000..c58c6f5
--- /dev/null
@@ -0,0 +1,149 @@
+<?php
+
+use MediaWiki\Config\ServiceOptions;
+
+/**
+ * @coversDefaultClass \MediaWiki\Config\ServiceOptions
+ */
+class ServiceOptionsTest extends \MediaWikiUnitTestCase {
+       public static $testObj;
+
+       public static function setUpBeforeClass() {
+               parent::setUpBeforeClass();
+
+               self::$testObj = new stdclass();
+       }
+
+       /**
+        * @dataProvider provideConstructor
+        * @covers ::__construct
+        * @covers ::assertRequiredOptions
+        * @covers ::get
+        */
+       public function testConstructor( $expected, $keys, ...$sources ) {
+               $options = new ServiceOptions( $keys, ...$sources );
+
+               foreach ( $expected as $key => $val ) {
+                       $this->assertSame( $val, $options->get( $key ) );
+               }
+
+               // This is lumped in the same test because there's no support for depending on a test that
+               // has a data provider.
+               $options->assertRequiredOptions( array_keys( $expected ) );
+
+               // Suppress warning if no assertions were run. This is expected for empty arguments.
+               $this->assertTrue( true );
+       }
+
+       public function provideConstructor() {
+               return [
+                       'No keys' => [ [], [], [ 'a' => 'aval' ] ],
+                       'Simple array source' => [
+                               [ 'a' => 'aval', 'b' => 'bval' ],
+                               [ 'a', 'b' ],
+                               [ 'a' => 'aval', 'b' => 'bval', 'c' => 'cval' ],
+                       ],
+                       'Simple HashConfig source' => [
+                               [ 'a' => 'aval', 'b' => 'bval' ],
+                               [ 'a', 'b' ],
+                               new HashConfig( [ 'a' => 'aval', 'b' => 'bval', 'c' => 'cval' ] ),
+                       ],
+                       'Three different sources' => [
+                               [ 'a' => 'aval', 'b' => 'bval' ],
+                               [ 'a', 'b' ],
+                               [ 'z' => 'zval' ],
+                               new HashConfig( [ 'a' => 'aval', 'c' => 'cval' ] ),
+                               [ 'b' => 'bval', 'd' => 'dval' ],
+                       ],
+                       'null key' => [
+                               [ 'a' => null ],
+                               [ 'a' ],
+                               [ 'a' => null ],
+                       ],
+                       'Numeric option name' => [
+                               [ '0' => 'nothing' ],
+                               [ '0' ],
+                               [ '0' => 'nothing' ],
+                       ],
+                       'Multiple sources for one key' => [
+                               [ 'a' => 'winner' ],
+                               [ 'a' ],
+                               [ 'a' => 'winner' ],
+                               [ 'a' => 'second place' ],
+                       ],
+                       'Object value is passed by reference' => [
+                               [ 'a' => self::$testObj ],
+                               [ 'a' ],
+                               [ 'a' => self::$testObj ],
+                       ],
+               ];
+       }
+
+       /**
+        * @covers ::__construct
+        */
+       public function testKeyNotFound() {
+               $this->setExpectedException( InvalidArgumentException::class,
+                       'Key "a" not found in input sources' );
+
+               new ServiceOptions( [ 'a' ], [ 'b' => 'bval' ], [ 'c' => 'cval' ] );
+       }
+
+       /**
+        * @covers ::__construct
+        * @covers ::assertRequiredOptions
+        */
+       public function testOutOfOrderAssertRequiredOptions() {
+               $options = new ServiceOptions( [ 'a', 'b' ], [ 'a' => '', 'b' => '' ] );
+               $options->assertRequiredOptions( [ 'b', 'a' ] );
+               $this->assertTrue( true, 'No exception thrown' );
+       }
+
+       /**
+        * @covers ::__construct
+        * @covers ::get
+        */
+       public function testGetUnrecognized() {
+               $this->setExpectedException( InvalidArgumentException::class,
+                       'Unrecognized option "b"' );
+
+               $options = new ServiceOptions( [ 'a' ], [ 'a' => '' ] );
+               $options->get( 'b' );
+       }
+
+       /**
+        * @covers ::__construct
+        * @covers ::assertRequiredOptions
+        */
+       public function testExtraKeys() {
+               $this->setExpectedException( Wikimedia\Assert\PreconditionException::class,
+                       'Precondition failed: Unsupported options passed: b, c!' );
+
+               $options = new ServiceOptions( [ 'a', 'b', 'c' ], [ 'a' => '', 'b' => '', 'c' => '' ] );
+               $options->assertRequiredOptions( [ 'a' ] );
+       }
+
+       /**
+        * @covers ::__construct
+        * @covers ::assertRequiredOptions
+        */
+       public function testMissingKeys() {
+               $this->setExpectedException( Wikimedia\Assert\PreconditionException::class,
+                       'Precondition failed: Required options missing: a, b!' );
+
+               $options = new ServiceOptions( [ 'c' ], [ 'c' => '' ] );
+               $options->assertRequiredOptions( [ 'a', 'b', 'c' ] );
+       }
+
+       /**
+        * @covers ::__construct
+        * @covers ::assertRequiredOptions
+        */
+       public function testExtraAndMissingKeys() {
+               $this->setExpectedException( Wikimedia\Assert\PreconditionException::class,
+                       'Precondition failed: Unsupported options passed: b! Required options missing: c!' );
+
+               $options = new ServiceOptions( [ 'a', 'b' ], [ 'a' => '', 'b' => '' ] );
+               $options->assertRequiredOptions( [ 'a', 'c' ] );
+       }
+}
diff --git a/tests/phpunit/unit/includes/content/JsonContentHandlerTest.php b/tests/phpunit/unit/includes/content/JsonContentHandlerTest.php
new file mode 100644 (file)
index 0000000..70db73c
--- /dev/null
@@ -0,0 +1,14 @@
+<?php
+
+class JsonContentHandlerTest extends \MediaWikiUnitTestCase {
+
+       /**
+        * @covers JsonContentHandler::makeEmptyContent
+        */
+       public function testMakeEmptyContent() {
+               $handler = new JsonContentHandler();
+               $content = $handler->makeEmptyContent();
+               $this->assertInstanceOf( JsonContent::class, $content );
+               $this->assertTrue( $content->isValid() );
+       }
+}
diff --git a/tests/phpunit/unit/includes/db/DatabaseOracleTest.php b/tests/phpunit/unit/includes/db/DatabaseOracleTest.php
new file mode 100644 (file)
index 0000000..061e121
--- /dev/null
@@ -0,0 +1,52 @@
+<?php
+
+class DatabaseOracleTest extends PHPUnit\Framework\TestCase {
+
+       use MediaWikiCoversValidator;
+       use PHPUnit4And6Compat;
+
+       /**
+        * @return PHPUnit_Framework_MockObject_MockObject|DatabaseOracle
+        */
+       private function getMockDb() {
+               return $this->getMockBuilder( DatabaseOracle::class )
+                       ->disableOriginalConstructor()
+                       ->setMethods( null )
+                       ->getMock();
+       }
+
+       public function provideBuildSubstring() {
+               yield [ 'someField', 1, 2, 'SUBSTR(someField,1,2)' ];
+               yield [ 'someField', 1, null, 'SUBSTR(someField,1)' ];
+       }
+
+       /**
+        * @covers DatabaseOracle::buildSubstring
+        * @dataProvider provideBuildSubstring
+        */
+       public function testBuildSubstring( $input, $start, $length, $expected ) {
+               $mockDb = $this->getMockDb();
+               $output = $mockDb->buildSubstring( $input, $start, $length );
+               $this->assertSame( $expected, $output );
+       }
+
+       public function provideBuildSubstring_invalidParams() {
+               yield [ -1, 1 ];
+               yield [ 1, -1 ];
+               yield [ 1, 'foo' ];
+               yield [ 'foo', 1 ];
+               yield [ null, 1 ];
+               yield [ 0, 1 ];
+       }
+
+       /**
+        * @covers DatabaseOracle::buildSubstring
+        * @dataProvider provideBuildSubstring_invalidParams
+        */
+       public function testBuildSubstring_invalidParams( $start, $length ) {
+               $mockDb = $this->getMockDb();
+               $this->setExpectedException( InvalidArgumentException::class );
+               $mockDb->buildSubstring( 'foo', $start, $length );
+       }
+
+}
diff --git a/tests/phpunit/unit/includes/debug/MWDebugTest.php b/tests/phpunit/unit/includes/debug/MWDebugTest.php
new file mode 100644 (file)
index 0000000..d29f44d
--- /dev/null
@@ -0,0 +1,140 @@
+<?php
+
+class MWDebugTest extends \MediaWikiUnitTestCase {
+
+       protected function setUp() {
+               parent::setUp();
+               /** Clear log before each test */
+               MWDebug::clearLog();
+       }
+
+       public static function setUpBeforeClass() {
+               parent::setUpBeforeClass();
+               MWDebug::init();
+               Wikimedia\suppressWarnings();
+       }
+
+       public static function tearDownAfterClass() {
+               parent::tearDownAfterClass();
+               MWDebug::deinit();
+               Wikimedia\restoreWarnings();
+       }
+
+       /**
+        * @covers MWDebug::log
+        */
+       public function testAddLog() {
+               MWDebug::log( 'logging a string' );
+               $this->assertEquals(
+                       [ [
+                               'msg' => 'logging a string',
+                               'type' => 'log',
+                               'caller' => 'MWDebugTest->testAddLog',
+                       ] ],
+                       MWDebug::getLog()
+               );
+       }
+
+       /**
+        * @covers MWDebug::warning
+        */
+       public function testAddWarning() {
+               MWDebug::warning( 'Warning message' );
+               $this->assertEquals(
+                       [ [
+                               'msg' => 'Warning message',
+                               'type' => 'warn',
+                               'caller' => 'MWDebugTest::testAddWarning',
+                       ] ],
+                       MWDebug::getLog()
+               );
+       }
+
+       /**
+        * @covers MWDebug::deprecated
+        */
+       public function testAvoidDuplicateDeprecations() {
+               MWDebug::deprecated( 'wfOldFunction', '1.0', 'component' );
+               MWDebug::deprecated( 'wfOldFunction', '1.0', 'component' );
+
+               // assertCount() not available on WMF integration server
+               $this->assertEquals( 1,
+                       count( MWDebug::getLog() ),
+                       "Only one deprecated warning per function should be kept"
+               );
+       }
+
+       /**
+        * @covers MWDebug::deprecated
+        */
+       public function testAvoidNonConsecutivesDuplicateDeprecations() {
+               MWDebug::deprecated( 'wfOldFunction', '1.0', 'component' );
+               MWDebug::warning( 'some warning' );
+               MWDebug::log( 'we could have logged something too' );
+               // Another deprecation
+               MWDebug::deprecated( 'wfOldFunction', '1.0', 'component' );
+
+               // assertCount() not available on WMF integration server
+               $this->assertEquals( 3,
+                       count( MWDebug::getLog() ),
+                       "Only one deprecated warning per function should be kept"
+               );
+       }
+
+       /**
+        * @covers MWDebug::appendDebugInfoToApiResult
+        */
+       public function testAppendDebugInfoToApiResultXmlFormat() {
+               $request = $this->newApiRequest(
+                       [ 'action' => 'help', 'format' => 'xml' ],
+                       '/api.php?action=help&format=xml'
+               );
+
+               $context = new RequestContext();
+               $context->setRequest( $request );
+
+               $apiMain = new ApiMain( $context );
+
+               $result = new ApiResult( $apiMain );
+
+               MWDebug::appendDebugInfoToApiResult( $context, $result );
+
+               $this->assertInstanceOf( ApiResult::class, $result );
+               $data = $result->getResultData();
+
+               $expectedKeys = [ 'mwVersion', 'phpEngine', 'phpVersion', 'gitRevision', 'gitBranch',
+                       'gitViewUrl', 'time', 'log', 'debugLog', 'queries', 'request', 'memory',
+                       'memoryPeak', 'includes', '_element' ];
+
+               foreach ( $expectedKeys as $expectedKey ) {
+                       $this->assertArrayHasKey( $expectedKey, $data['debuginfo'], "debuginfo has $expectedKey" );
+               }
+
+               $xml = ApiFormatXml::recXmlPrint( 'help', $data, null );
+
+               // exception not thrown
+               $this->assertInternalType( 'string', $xml );
+       }
+
+       /**
+        * @param string[] $params
+        * @param string $requestUrl
+        *
+        * @return FauxRequest
+        */
+       private function newApiRequest( array $params, $requestUrl ) {
+               $request = $this->getMockBuilder( FauxRequest::class )
+                       ->setMethods( [ 'getRequestURL' ] )
+                       ->setConstructorArgs( [
+                               $params
+                       ] )
+                       ->getMock();
+
+               $request->expects( $this->any() )
+                       ->method( 'getRequestURL' )
+                       ->will( $this->returnValue( $requestUrl ) );
+
+               return $request;
+       }
+
+}
diff --git a/tests/phpunit/unit/includes/debug/logger/MonologSpiTest.php b/tests/phpunit/unit/includes/debug/logger/MonologSpiTest.php
new file mode 100644 (file)
index 0000000..ecb5d17
--- /dev/null
@@ -0,0 +1,135 @@
+<?php
+/**
+ * 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; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * 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.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+namespace MediaWiki\Logger;
+
+use Wikimedia\TestingAccessWrapper;
+
+class MonologSpiTest extends \MediaWikiUnitTestCase {
+
+       /**
+        * @covers MediaWiki\Logger\MonologSpi::mergeConfig
+        */
+       public function testMergeConfig() {
+               $base = [
+                       'loggers' => [
+                               '@default' => [
+                                       'processors' => [ 'constructor' ],
+                                       'handlers' => [ 'constructor' ],
+                               ],
+                       ],
+                       'processors' => [
+                               'constructor' => [
+                                       'class' => 'constructor',
+                               ],
+                       ],
+                       'handlers' => [
+                               'constructor' => [
+                                       'class' => 'constructor',
+                                       'formatter' => 'constructor',
+                               ],
+                       ],
+                       'formatters' => [
+                               'constructor' => [
+                                       'class' => 'constructor',
+                               ],
+                       ],
+               ];
+
+               $fixture = new MonologSpi( $base );
+               $this->assertSame(
+                       $base,
+                       TestingAccessWrapper::newFromObject( $fixture )->config
+               );
+
+               $fixture->mergeConfig( [
+                       'loggers' => [
+                               'merged' => [
+                                       'processors' => [ 'merged' ],
+                                       'handlers' => [ 'merged' ],
+                               ],
+                       ],
+                       'processors' => [
+                               'merged' => [
+                                       'class' => 'merged',
+                               ],
+                       ],
+                       'magic' => [
+                               'idkfa' => [ 'xyzzy' ],
+                       ],
+                       'handlers' => [
+                               'merged' => [
+                                       'class' => 'merged',
+                                       'formatter' => 'merged',
+                               ],
+                       ],
+                       'formatters' => [
+                               'merged' => [
+                                       'class' => 'merged',
+                               ],
+                       ],
+               ] );
+               $this->assertSame(
+                       [
+                               'loggers' => [
+                                       '@default' => [
+                                               'processors' => [ 'constructor' ],
+                                               'handlers' => [ 'constructor' ],
+                                       ],
+                                       'merged' => [
+                                               'processors' => [ 'merged' ],
+                                               'handlers' => [ 'merged' ],
+                                       ],
+                               ],
+                               'processors' => [
+                                       'constructor' => [
+                                               'class' => 'constructor',
+                                       ],
+                                       'merged' => [
+                                               'class' => 'merged',
+                                       ],
+                               ],
+                               'handlers' => [
+                                       'constructor' => [
+                                               'class' => 'constructor',
+                                               'formatter' => 'constructor',
+                                       ],
+                                       'merged' => [
+                                               'class' => 'merged',
+                                               'formatter' => 'merged',
+                                       ],
+                               ],
+                               'formatters' => [
+                                       'constructor' => [
+                                               'class' => 'constructor',
+                                       ],
+                                       'merged' => [
+                                               'class' => 'merged',
+                                       ],
+                               ],
+                               'magic' => [
+                                       'idkfa' => [ 'xyzzy' ],
+                               ],
+                       ],
+                       TestingAccessWrapper::newFromObject( $fixture )->config
+               );
+       }
+
+}
diff --git a/tests/phpunit/unit/includes/debug/logger/monolog/AvroFormatterTest.php b/tests/phpunit/unit/includes/debug/logger/monolog/AvroFormatterTest.php
new file mode 100644 (file)
index 0000000..e091561
--- /dev/null
@@ -0,0 +1,75 @@
+<?php
+/**
+ * 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; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * 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.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+namespace MediaWiki\Logger\Monolog;
+
+use PHPUnit_Framework_Error_Notice;
+
+/**
+ * @covers \MediaWiki\Logger\Monolog\AvroFormatter
+ */
+class AvroFormatterTest extends \MediaWikiUnitTestCase {
+
+       protected function setUp() {
+               if ( !class_exists( 'AvroStringIO' ) ) {
+                       $this->markTestSkipped( 'Avro is required for the AvroFormatterTest' );
+               }
+               parent::setUp();
+       }
+
+       public function testSchemaNotAvailable() {
+               $formatter = new AvroFormatter( [] );
+               $this->setExpectedException(
+                       'PHPUnit_Framework_Error_Notice',
+                       "The schema for channel 'marty' is not available"
+               );
+               $formatter->format( [ 'channel' => 'marty' ] );
+       }
+
+       public function testSchemaNotAvailableReturnValue() {
+               $formatter = new AvroFormatter( [] );
+               $noticeEnabled = PHPUnit_Framework_Error_Notice::$enabled;
+               // disable conversion of notices
+               PHPUnit_Framework_Error_Notice::$enabled = false;
+               // have to keep the user notice from being output
+               \Wikimedia\suppressWarnings();
+               $res = $formatter->format( [ 'channel' => 'marty' ] );
+               \Wikimedia\restoreWarnings();
+               PHPUnit_Framework_Error_Notice::$enabled = $noticeEnabled;
+               $this->assertNull( $res );
+       }
+
+       public function testDoesSomethingWhenSchemaAvailable() {
+               $formatter = new AvroFormatter( [
+                       'string' => [
+                               'schema' => [ 'type' => 'string' ],
+                               'revision' => 1010101,
+                       ]
+               ] );
+               $res = $formatter->format( [
+                       'channel' => 'string',
+                       'context' => 'better to be',
+               ] );
+               $this->assertNotNull( $res );
+               // basically just tell us if avro changes its string encoding, or if
+               // we completely fail to generate a log message.
+               $this->assertEquals( 'AAAAAAAAD2m1GGJldHRlciB0byBiZQ==', base64_encode( $res ) );
+       }
+}
diff --git a/tests/phpunit/unit/includes/debug/logger/monolog/CeeFormatterTest.php b/tests/phpunit/unit/includes/debug/logger/monolog/CeeFormatterTest.php
new file mode 100644 (file)
index 0000000..b30c7a4
--- /dev/null
@@ -0,0 +1,20 @@
+<?php
+
+namespace MediaWiki\Logger\Monolog;
+
+/**
+ * Flay per https://phabricator.wikimedia.org/T218688.
+ *
+ * @group Broken
+ * @covers \MediaWiki\Logger\Monolog\CeeFormatter
+ */
+class CeeFormatterTest extends \PHPUnit\Framework\TestCase {
+       public function testV1() {
+               $ls_formatter = new LogstashFormatter( 'app', 'system', null, 'ctx_', LogstashFormatter::V1 );
+               $cee_formatter = new CeeFormatter( 'app', 'system', null, 'ctx_', LogstashFormatter::V1 );
+               $record = [ 'extra' => [ 'url' => 1 ], 'context' => [ 'url' => 2 ] ];
+               $this->assertSame(
+                       $cee_formatter->format( $record ),
+                       "@cee: " . $ls_formatter->format( $record ) );
+       }
+}
diff --git a/tests/phpunit/unit/includes/debug/logger/monolog/KafkaHandlerTest.php b/tests/phpunit/unit/includes/debug/logger/monolog/KafkaHandlerTest.php
new file mode 100644 (file)
index 0000000..bbac17f
--- /dev/null
@@ -0,0 +1,226 @@
+<?php
+/**
+ * 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; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * 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.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+namespace MediaWiki\Logger\Monolog;
+
+use Monolog\Logger;
+use Wikimedia\TestingAccessWrapper;
+
+/**
+ * @covers \MediaWiki\Logger\Monolog\KafkaHandler
+ */
+class KafkaHandlerTest extends \MediaWikiUnitTestCase {
+
+       protected function setUp() {
+               if ( !class_exists( 'Monolog\Handler\AbstractProcessingHandler' )
+                       || !class_exists( 'Kafka\Produce' )
+               ) {
+                       $this->markTestSkipped( 'Monolog and Kafka are required for the KafkaHandlerTest' );
+               }
+
+               parent::setUp();
+       }
+
+       public function topicNamingProvider() {
+               return [
+                       [ [], 'monolog_foo' ],
+                       [ [ 'alias' => [ 'foo' => 'bar' ] ], 'bar' ]
+               ];
+       }
+
+       /**
+        * @dataProvider topicNamingProvider
+        */
+       public function testTopicNaming( $options, $expect ) {
+               $produce = $this->getMockBuilder( 'Kafka\Produce' )
+                       ->disableOriginalConstructor()
+                       ->getMock();
+               $produce->expects( $this->any() )
+                       ->method( 'getAvailablePartitions' )
+                       ->will( $this->returnValue( [ 'A' ] ) );
+               $produce->expects( $this->once() )
+                       ->method( 'setMessages' )
+                       ->with( $expect, $this->anything(), $this->anything() );
+               $produce->expects( $this->any() )
+                       ->method( 'send' )
+                       ->will( $this->returnValue( true ) );
+
+               $handler = new KafkaHandler( $produce, $options );
+               $handler->handle( [
+                       'channel' => 'foo',
+                       'level' => Logger::EMERGENCY,
+                       'extra' => [],
+                       'context' => [],
+               ] );
+       }
+
+       public function swallowsExceptionsWhenRequested() {
+               return [
+                       // defaults to false
+                       [ [], true ],
+                       // also try false explicitly
+                       [ [ 'swallowExceptions' => false ], true ],
+                       // turn it on
+                       [ [ 'swallowExceptions' => true ], false ],
+               ];
+       }
+
+       /**
+        * @dataProvider swallowsExceptionsWhenRequested
+        */
+       public function testGetAvailablePartitionsException( $options, $expectException ) {
+               $produce = $this->getMockBuilder( 'Kafka\Produce' )
+                       ->disableOriginalConstructor()
+                       ->getMock();
+               $produce->expects( $this->any() )
+                       ->method( 'getAvailablePartitions' )
+                       ->will( $this->throwException( new \Kafka\Exception ) );
+               $produce->expects( $this->any() )
+                       ->method( 'send' )
+                       ->will( $this->returnValue( true ) );
+
+               if ( $expectException ) {
+                       $this->setExpectedException( 'Kafka\Exception' );
+               }
+
+               $handler = new KafkaHandler( $produce, $options );
+               $handler->handle( [
+                       'channel' => 'foo',
+                       'level' => Logger::EMERGENCY,
+                       'extra' => [],
+                       'context' => [],
+               ] );
+
+               if ( !$expectException ) {
+                       $this->assertTrue( true, 'no exception was thrown' );
+               }
+       }
+
+       /**
+        * @dataProvider swallowsExceptionsWhenRequested
+        */
+       public function testSendException( $options, $expectException ) {
+               $produce = $this->getMockBuilder( 'Kafka\Produce' )
+                       ->disableOriginalConstructor()
+                       ->getMock();
+               $produce->expects( $this->any() )
+                       ->method( 'getAvailablePartitions' )
+                       ->will( $this->returnValue( [ 'A' ] ) );
+               $produce->expects( $this->any() )
+                       ->method( 'send' )
+                       ->will( $this->throwException( new \Kafka\Exception ) );
+
+               if ( $expectException ) {
+                       $this->setExpectedException( 'Kafka\Exception' );
+               }
+
+               $handler = new KafkaHandler( $produce, $options );
+               $handler->handle( [
+                       'channel' => 'foo',
+                       'level' => Logger::EMERGENCY,
+                       'extra' => [],
+                       'context' => [],
+               ] );
+
+               if ( !$expectException ) {
+                       $this->assertTrue( true, 'no exception was thrown' );
+               }
+       }
+
+       public function testHandlesNullFormatterResult() {
+               $produce = $this->getMockBuilder( 'Kafka\Produce' )
+                       ->disableOriginalConstructor()
+                       ->getMock();
+               $produce->expects( $this->any() )
+                       ->method( 'getAvailablePartitions' )
+                       ->will( $this->returnValue( [ 'A' ] ) );
+               $mockMethod = $produce->expects( $this->exactly( 2 ) )
+                       ->method( 'setMessages' );
+               $produce->expects( $this->any() )
+                       ->method( 'send' )
+                       ->will( $this->returnValue( true ) );
+               // evil hax
+               $matcher = TestingAccessWrapper::newFromObject( $mockMethod )->matcher;
+               TestingAccessWrapper::newFromObject( $matcher )->parametersMatcher =
+                       new \PHPUnit_Framework_MockObject_Matcher_ConsecutiveParameters( [
+                               [ $this->anything(), $this->anything(), [ 'words' ] ],
+                               [ $this->anything(), $this->anything(), [ 'lines' ] ]
+                       ] );
+
+               $formatter = $this->createMock( \Monolog\Formatter\FormatterInterface::class );
+               $formatter->expects( $this->any() )
+                       ->method( 'format' )
+                       ->will( $this->onConsecutiveCalls( 'words', null, 'lines' ) );
+
+               $handler = new KafkaHandler( $produce, [] );
+               $handler->setFormatter( $formatter );
+               for ( $i = 0; $i < 3; ++$i ) {
+                       $handler->handle( [
+                               'channel' => 'foo',
+                               'level' => Logger::EMERGENCY,
+                               'extra' => [],
+                               'context' => [],
+                       ] );
+               }
+       }
+
+       public function testBatchHandlesNullFormatterResult() {
+               $produce = $this->getMockBuilder( 'Kafka\Produce' )
+                       ->disableOriginalConstructor()
+                       ->getMock();
+               $produce->expects( $this->any() )
+                       ->method( 'getAvailablePartitions' )
+                       ->will( $this->returnValue( [ 'A' ] ) );
+               $produce->expects( $this->once() )
+                       ->method( 'setMessages' )
+                       ->with( $this->anything(), $this->anything(), [ 'words', 'lines' ] );
+               $produce->expects( $this->any() )
+                       ->method( 'send' )
+                       ->will( $this->returnValue( true ) );
+
+               $formatter = $this->createMock( \Monolog\Formatter\FormatterInterface::class );
+               $formatter->expects( $this->any() )
+                       ->method( 'format' )
+                       ->will( $this->onConsecutiveCalls( 'words', null, 'lines' ) );
+
+               $handler = new KafkaHandler( $produce, [] );
+               $handler->setFormatter( $formatter );
+               $handler->handleBatch( [
+                       [
+                               'channel' => 'foo',
+                               'level' => Logger::EMERGENCY,
+                               'extra' => [],
+                               'context' => [],
+                       ],
+                       [
+                               'channel' => 'foo',
+                               'level' => Logger::EMERGENCY,
+                               'extra' => [],
+                               'context' => [],
+                       ],
+                       [
+                               'channel' => 'foo',
+                               'level' => Logger::EMERGENCY,
+                               'extra' => [],
+                               'context' => [],
+                       ],
+               ] );
+       }
+}
diff --git a/tests/phpunit/unit/includes/debug/logger/monolog/LineFormatterTest.php b/tests/phpunit/unit/includes/debug/logger/monolog/LineFormatterTest.php
new file mode 100644 (file)
index 0000000..8da3d93
--- /dev/null
@@ -0,0 +1,121 @@
+<?php
+/**
+ * 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; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * 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.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+namespace MediaWiki\Logger\Monolog;
+
+use AssertionError;
+use InvalidArgumentException;
+use LengthException;
+use LogicException;
+use Wikimedia\TestingAccessWrapper;
+
+class LineFormatterTest extends \MediaWikiUnitTestCase {
+
+       protected function setUp() {
+               if ( !class_exists( 'Monolog\Formatter\LineFormatter' ) ) {
+                       $this->markTestSkipped( 'This test requires monolog to be installed' );
+               }
+               parent::setUp();
+       }
+
+       /**
+        * @covers MediaWiki\Logger\Monolog\LineFormatter::normalizeException
+        */
+       public function testNormalizeExceptionNoTrace() {
+               $fixture = new LineFormatter();
+               $fixture->includeStacktraces( false );
+               $fixture = TestingAccessWrapper::newFromObject( $fixture );
+               $boom = new InvalidArgumentException( 'boom', 0,
+                       new LengthException( 'too long', 0,
+                               new LogicException( 'Spock wuz here' )
+                       )
+               );
+               $out = $fixture->normalizeException( $boom );
+               $this->assertContains( "\n[Exception InvalidArgumentException]", $out );
+               $this->assertContains( "\nCaused by: [Exception LengthException]", $out );
+               $this->assertContains( "\nCaused by: [Exception LogicException]", $out );
+               $this->assertNotContains( "\n  #0", $out );
+       }
+
+       /**
+        * @covers MediaWiki\Logger\Monolog\LineFormatter::normalizeException
+        */
+       public function testNormalizeExceptionTrace() {
+               $fixture = new LineFormatter();
+               $fixture->includeStacktraces( true );
+               $fixture = TestingAccessWrapper::newFromObject( $fixture );
+               $boom = new InvalidArgumentException( 'boom', 0,
+                       new LengthException( 'too long', 0,
+                               new LogicException( 'Spock wuz here' )
+                       )
+               );
+               $out = $fixture->normalizeException( $boom );
+               $this->assertContains( "\n[Exception InvalidArgumentException]", $out );
+               $this->assertContains( "\nCaused by: [Exception LengthException]", $out );
+               $this->assertContains( "\nCaused by: [Exception LogicException]", $out );
+               $this->assertContains( "\n  #0", $out );
+       }
+
+       /**
+        * @covers MediaWiki\Logger\Monolog\LineFormatter::normalizeException
+        */
+       public function testNormalizeExceptionErrorNoTrace() {
+               if ( !class_exists( AssertionError::class ) ) {
+                       $this->markTestSkipped( 'AssertionError class does not exist' );
+               }
+
+               $fixture = new LineFormatter();
+               $fixture->includeStacktraces( false );
+               $fixture = TestingAccessWrapper::newFromObject( $fixture );
+               $boom = new InvalidArgumentException( 'boom', 0,
+                       new LengthException( 'too long', 0,
+                               new AssertionError( 'Spock wuz here' )
+                       )
+               );
+               $out = $fixture->normalizeException( $boom );
+               $this->assertContains( "\n[Exception InvalidArgumentException]", $out );
+               $this->assertContains( "\nCaused by: [Exception LengthException]", $out );
+               $this->assertContains( "\nCaused by: [Error AssertionError]", $out );
+               $this->assertNotContains( "\n  #0", $out );
+       }
+
+       /**
+        * @covers MediaWiki\Logger\Monolog\LineFormatter::normalizeException
+        */
+       public function testNormalizeExceptionErrorTrace() {
+               if ( !class_exists( AssertionError::class ) ) {
+                       $this->markTestSkipped( 'AssertionError class does not exist' );
+               }
+
+               $fixture = new LineFormatter();
+               $fixture->includeStacktraces( true );
+               $fixture = TestingAccessWrapper::newFromObject( $fixture );
+               $boom = new InvalidArgumentException( 'boom', 0,
+                       new LengthException( 'too long', 0,
+                               new AssertionError( 'Spock wuz here' )
+                       )
+               );
+               $out = $fixture->normalizeException( $boom );
+               $this->assertContains( "\n[Exception InvalidArgumentException]", $out );
+               $this->assertContains( "\nCaused by: [Exception LengthException]", $out );
+               $this->assertContains( "\nCaused by: [Error AssertionError]", $out );
+               $this->assertContains( "\n  #0", $out );
+       }
+}
diff --git a/tests/phpunit/unit/includes/debug/logger/monolog/LogstashFormatterTest.php b/tests/phpunit/unit/includes/debug/logger/monolog/LogstashFormatterTest.php
new file mode 100644 (file)
index 0000000..a1207b2
--- /dev/null
@@ -0,0 +1,59 @@
+<?php
+
+namespace MediaWiki\Logger\Monolog;
+
+class LogstashFormatterTest extends \PHPUnit\Framework\TestCase {
+       /**
+        * @dataProvider provideV1
+        * @covers MediaWiki\Logger\Monolog\LogstashFormatter::formatV1
+        * @param array $record The input record.
+        * @param array $expected Associative array of expected keys and their values.
+        * @param array $notExpected List of keys that should not exist.
+        */
+       public function testV1( array $record, array $expected, array $notExpected ) {
+               $formatter = new LogstashFormatter( 'app', 'system', null, null, LogstashFormatter::V1 );
+               $formatted = json_decode( $formatter->format( $record ), true );
+               foreach ( $expected as $key => $value ) {
+                       $this->assertArrayHasKey( $key, $formatted );
+                       $this->assertSame( $value, $formatted[$key] );
+               }
+               foreach ( $notExpected as $key ) {
+                       $this->assertArrayNotHasKey( $key, $formatted );
+               }
+       }
+
+       public function provideV1() {
+               return [
+                       [
+                               [ 'extra' => [ 'foo' => 1 ], 'context' => [ 'bar' => 2 ] ],
+                               [ 'foo' => 1, 'bar' => 2 ],
+                               [ 'logstash_formatter_key_conflict' ],
+                       ],
+                       [
+                               [ 'extra' => [ 'url' => 1 ], 'context' => [ 'url' => 2 ] ],
+                               [ 'url' => 1, 'c_url' => 2, 'logstash_formatter_key_conflict' => [ 'url' ] ],
+                               [],
+                       ],
+                       [
+                               [ 'channel' => 'x', 'context' => [ 'channel' => 'y' ] ],
+                               [ 'channel' => 'x', 'c_channel' => 'y',
+                                       'logstash_formatter_key_conflict' => [ 'channel' ] ],
+                               [],
+                       ],
+               ];
+       }
+
+       /**
+        * @covers MediaWiki\Logger\Monolog\LogstashFormatter::formatV1
+        */
+       public function testV1WithPrefix() {
+               $formatter = new LogstashFormatter( 'app', 'system', null, 'ctx_', LogstashFormatter::V1 );
+               $record = [ 'extra' => [ 'url' => 1 ], 'context' => [ 'url' => 2 ] ];
+               $formatted = json_decode( $formatter->format( $record ), true );
+               $this->assertArrayHasKey( 'url', $formatted );
+               $this->assertSame( 1, $formatted['url'] );
+               $this->assertArrayHasKey( 'ctx_url', $formatted );
+               $this->assertSame( 2, $formatted['ctx_url'] );
+               $this->assertArrayNotHasKey( 'c_url', $formatted );
+       }
+}
diff --git a/tests/phpunit/unit/includes/deferred/MWCallableUpdateTest.php b/tests/phpunit/unit/includes/deferred/MWCallableUpdateTest.php
new file mode 100644 (file)
index 0000000..3ab9b56
--- /dev/null
@@ -0,0 +1,82 @@
+<?php
+
+/**
+ * @covers MWCallableUpdate
+ */
+class MWCallableUpdateTest extends PHPUnit\Framework\TestCase {
+
+       use MediaWikiCoversValidator;
+
+       public function testDoUpdate() {
+               $ran = 0;
+               $update = new MWCallableUpdate( function () use ( &$ran ) {
+                       $ran++;
+               } );
+               $this->assertSame( 0, $ran );
+               $update->doUpdate();
+               $this->assertSame( 1, $ran );
+       }
+
+       public function testCancel() {
+               // Prepare update and DB
+               $db = new DatabaseTestHelper( __METHOD__ );
+               $db->begin( __METHOD__ );
+               $ran = 0;
+               $update = new MWCallableUpdate( function () use ( &$ran ) {
+                       $ran++;
+               }, __METHOD__, $db );
+
+               // Emulate rollback
+               $db->rollback( __METHOD__ );
+
+               $update->doUpdate();
+
+               // Ensure it was cancelled
+               $this->assertSame( 0, $ran );
+       }
+
+       public function testCancelSome() {
+               // Prepare update and DB
+               $db1 = new DatabaseTestHelper( __METHOD__ );
+               $db1->begin( __METHOD__ );
+               $db2 = new DatabaseTestHelper( __METHOD__ );
+               $db2->begin( __METHOD__ );
+               $ran = 0;
+               $update = new MWCallableUpdate( function () use ( &$ran ) {
+                       $ran++;
+               }, __METHOD__, [ $db1, $db2 ] );
+
+               // Emulate rollback
+               $db1->rollback( __METHOD__ );
+
+               $update->doUpdate();
+
+               // Prevents: "Notice: DB transaction writes or callbacks still pending"
+               $db2->rollback( __METHOD__ );
+
+               // Ensure it was cancelled
+               $this->assertSame( 0, $ran );
+       }
+
+       public function testCancelAll() {
+               // Prepare update and DB
+               $db1 = new DatabaseTestHelper( __METHOD__ );
+               $db1->begin( __METHOD__ );
+               $db2 = new DatabaseTestHelper( __METHOD__ );
+               $db2->begin( __METHOD__ );
+               $ran = 0;
+               $update = new MWCallableUpdate( function () use ( &$ran ) {
+                       $ran++;
+               }, __METHOD__, [ $db1, $db2 ] );
+
+               // Emulate rollbacks
+               $db1->rollback( __METHOD__ );
+               $db2->rollback( __METHOD__ );
+
+               $update->doUpdate();
+
+               // Ensure it was cancelled
+               $this->assertSame( 0, $ran );
+       }
+
+}
diff --git a/tests/phpunit/unit/includes/deferred/TransactionRoundDefiningUpdateTest.php b/tests/phpunit/unit/includes/deferred/TransactionRoundDefiningUpdateTest.php
new file mode 100644 (file)
index 0000000..693897e
--- /dev/null
@@ -0,0 +1,19 @@
+<?php
+
+/**
+ * @covers TransactionRoundDefiningUpdate
+ */
+class TransactionRoundDefiningUpdateTest extends PHPUnit\Framework\TestCase {
+
+       use MediaWikiCoversValidator;
+
+       public function testDoUpdate() {
+               $ran = 0;
+               $update = new TransactionRoundDefiningUpdate( function () use ( &$ran ) {
+                       $ran++;
+               } );
+               $this->assertSame( 0, $ran );
+               $update->doUpdate();
+               $this->assertSame( 1, $ran );
+       }
+}
diff --git a/tests/phpunit/unit/includes/diff/ArrayDiffFormatterTest.php b/tests/phpunit/unit/includes/diff/ArrayDiffFormatterTest.php
new file mode 100644 (file)
index 0000000..d436991
--- /dev/null
@@ -0,0 +1,134 @@
+<?php
+
+/**
+ * @author Addshore
+ *
+ * @group Diff
+ */
+class ArrayDiffFormatterTest extends \MediaWikiUnitTestCase {
+
+       /**
+        * @param Diff $input
+        * @param array $expectedOutput
+        * @dataProvider provideTestFormat
+        * @covers ArrayDiffFormatter::format
+        */
+       public function testFormat( $input, $expectedOutput ) {
+               $instance = new ArrayDiffFormatter();
+               $output = $instance->format( $input );
+               $this->assertEquals( $expectedOutput, $output );
+       }
+
+       private function getMockDiff( $edits ) {
+               $diff = $this->getMockBuilder( Diff::class )
+                       ->disableOriginalConstructor()
+                       ->getMock();
+               $diff->expects( $this->any() )
+                       ->method( 'getEdits' )
+                       ->will( $this->returnValue( $edits ) );
+               return $diff;
+       }
+
+       private function getMockDiffOp( $type = null, $orig = [], $closing = [] ) {
+               $diffOp = $this->getMockBuilder( DiffOp::class )
+                       ->disableOriginalConstructor()
+                       ->getMock();
+               $diffOp->expects( $this->any() )
+                       ->method( 'getType' )
+                       ->will( $this->returnValue( $type ) );
+               $diffOp->expects( $this->any() )
+                       ->method( 'getOrig' )
+                       ->will( $this->returnValue( $orig ) );
+               if ( $type === 'change' ) {
+                       $diffOp->expects( $this->any() )
+                               ->method( 'getClosing' )
+                               ->with( $this->isType( 'integer' ) )
+                               ->will( $this->returnCallback( function () {
+                                       return 'mockLine';
+                               } ) );
+               } else {
+                       $diffOp->expects( $this->any() )
+                               ->method( 'getClosing' )
+                               ->will( $this->returnValue( $closing ) );
+               }
+               return $diffOp;
+       }
+
+       public function provideTestFormat() {
+               $emptyArrayTestCases = [
+                       $this->getMockDiff( [] ),
+                       $this->getMockDiff( [ $this->getMockDiffOp( 'add' ) ] ),
+                       $this->getMockDiff( [ $this->getMockDiffOp( 'delete' ) ] ),
+                       $this->getMockDiff( [ $this->getMockDiffOp( 'change' ) ] ),
+                       $this->getMockDiff( [ $this->getMockDiffOp( 'copy' ) ] ),
+                       $this->getMockDiff( [ $this->getMockDiffOp( 'FOOBARBAZ' ) ] ),
+                       $this->getMockDiff( [ $this->getMockDiffOp( 'add', 'line' ) ] ),
+                       $this->getMockDiff( [ $this->getMockDiffOp( 'delete', [], [ 'line' ] ) ] ),
+                       $this->getMockDiff( [ $this->getMockDiffOp( 'copy', [], [ 'line' ] ) ] ),
+               ];
+
+               $otherTestCases = [];
+               $otherTestCases[] = [
+                       $this->getMockDiff( [ $this->getMockDiffOp( 'add', [], [ 'a1' ] ) ] ),
+                       [ [ 'action' => 'add', 'new' => 'a1', 'newline' => 1 ] ],
+               ];
+               $otherTestCases[] = [
+                       $this->getMockDiff( [ $this->getMockDiffOp( 'add', [], [ 'a1', 'a2' ] ) ] ),
+                       [
+                               [ 'action' => 'add', 'new' => 'a1', 'newline' => 1 ],
+                               [ 'action' => 'add', 'new' => 'a2', 'newline' => 2 ],
+                       ],
+               ];
+               $otherTestCases[] = [
+                       $this->getMockDiff( [ $this->getMockDiffOp( 'delete', [ 'd1' ] ) ] ),
+                       [ [ 'action' => 'delete', 'old' => 'd1', 'oldline' => 1 ] ],
+               ];
+               $otherTestCases[] = [
+                       $this->getMockDiff( [ $this->getMockDiffOp( 'delete', [ 'd1', 'd2' ] ) ] ),
+                       [
+                               [ 'action' => 'delete', 'old' => 'd1', 'oldline' => 1 ],
+                               [ 'action' => 'delete', 'old' => 'd2', 'oldline' => 2 ],
+                       ],
+               ];
+               $otherTestCases[] = [
+                       $this->getMockDiff( [ $this->getMockDiffOp( 'change', [ 'd1' ], [ 'a1' ] ) ] ),
+                       [ [
+                               'action' => 'change',
+                               'old' => 'd1',
+                               'new' => 'mockLine',
+                               'newline' => 1, 'oldline' => 1
+                       ] ],
+               ];
+               $otherTestCases[] = [
+                       $this->getMockDiff( [ $this->getMockDiffOp(
+                               'change',
+                               [ 'd1', 'd2' ],
+                               [ 'a1', 'a2' ]
+                       ) ] ),
+                       [
+                               [
+                                       'action' => 'change',
+                                       'old' => 'd1',
+                                       'new' => 'mockLine',
+                                       'newline' => 1, 'oldline' => 1
+                               ],
+                               [
+                                       'action' => 'change',
+                                       'old' => 'd2',
+                                       'new' => 'mockLine',
+                                       'newline' => 2, 'oldline' => 2
+                               ],
+                       ],
+               ];
+
+               $testCases = [];
+               foreach ( $emptyArrayTestCases as $testCase ) {
+                       $testCases[] = [ $testCase, [] ];
+               }
+               foreach ( $otherTestCases as $testCase ) {
+                       $testCases[] = [ $testCase[0], $testCase[1] ];
+               }
+               return $testCases;
+       }
+
+}
diff --git a/tests/phpunit/unit/includes/diff/DiffOpTest.php b/tests/phpunit/unit/includes/diff/DiffOpTest.php
new file mode 100644 (file)
index 0000000..4e1aced
--- /dev/null
@@ -0,0 +1,68 @@
+<?php
+/**
+ * @author Addshore
+ *
+ * @group Diff
+ */
+class DiffOpTest extends \MediaWikiUnitTestCase {
+
+       /**
+        * @covers DiffOp::getType
+        */
+       public function testGetType() {
+               $obj = new FakeDiffOp();
+               $obj->type = 'foo';
+               $this->assertEquals( 'foo', $obj->getType() );
+       }
+
+       /**
+        * @covers DiffOp::getOrig
+        */
+       public function testGetOrig() {
+               $obj = new FakeDiffOp();
+               $obj->orig = [ 'foo' ];
+               $this->assertEquals( [ 'foo' ], $obj->getOrig() );
+       }
+
+       /**
+        * @covers DiffOp::getClosing
+        */
+       public function testGetClosing() {
+               $obj = new FakeDiffOp();
+               $obj->closing = [ 'foo' ];
+               $this->assertEquals( [ 'foo' ], $obj->getClosing() );
+       }
+
+       /**
+        * @covers DiffOp::getClosing
+        */
+       public function testGetClosingWithParameter() {
+               $obj = new FakeDiffOp();
+               $obj->closing = [ 'foo', 'bar', 'baz' ];
+               $this->assertEquals( 'foo', $obj->getClosing( 0 ) );
+               $this->assertEquals( 'bar', $obj->getClosing( 1 ) );
+               $this->assertEquals( 'baz', $obj->getClosing( 2 ) );
+               $this->assertEquals( null, $obj->getClosing( 3 ) );
+       }
+
+       /**
+        * @covers DiffOp::norig
+        */
+       public function testNorig() {
+               $obj = new FakeDiffOp();
+               $this->assertEquals( 0, $obj->norig() );
+               $obj->orig = [ 'foo' ];
+               $this->assertEquals( 1, $obj->norig() );
+       }
+
+       /**
+        * @covers DiffOp::nclosing
+        */
+       public function testNclosing() {
+               $obj = new FakeDiffOp();
+               $this->assertEquals( 0, $obj->nclosing() );
+               $obj->closing = [ 'foo' ];
+               $this->assertEquals( 1, $obj->nclosing() );
+       }
+
+}
diff --git a/tests/phpunit/unit/includes/diff/DiffTest.php b/tests/phpunit/unit/includes/diff/DiffTest.php
new file mode 100644 (file)
index 0000000..f0a8490
--- /dev/null
@@ -0,0 +1,19 @@
+<?php
+
+/**
+ * @author Addshore
+ *
+ * @group Diff
+ */
+class DiffTest extends \MediaWikiUnitTestCase {
+
+       /**
+        * @covers Diff::getEdits
+        */
+       public function testGetEdits() {
+               $obj = new Diff( [], [] );
+               $obj->edits = 'FooBarBaz';
+               $this->assertEquals( 'FooBarBaz', $obj->getEdits() );
+       }
+
+}
diff --git a/tests/phpunit/unit/includes/diff/DifferenceEngineSlotDiffRendererTest.php b/tests/phpunit/unit/includes/diff/DifferenceEngineSlotDiffRendererTest.php
new file mode 100644 (file)
index 0000000..fe129b7
--- /dev/null
@@ -0,0 +1,44 @@
+<?php
+
+/**
+ * @covers DifferenceEngineSlotDiffRenderer
+ */
+class DifferenceEngineSlotDiffRendererTest extends \PHPUnit\Framework\TestCase {
+
+       public function testGetDiff() {
+               $differenceEngine = new CustomDifferenceEngine();
+               $slotDiffRenderer = new DifferenceEngineSlotDiffRenderer( $differenceEngine );
+               $oldContent = ContentHandler::makeContent( 'xxx', null, CONTENT_MODEL_TEXT );
+               $newContent = ContentHandler::makeContent( 'yyy', null, CONTENT_MODEL_TEXT );
+
+               $diff = $slotDiffRenderer->getDiff( $oldContent, $newContent );
+               $this->assertEquals( 'xxx|yyy', $diff );
+
+               $diff = $slotDiffRenderer->getDiff( null, $newContent );
+               $this->assertEquals( '|yyy', $diff );
+
+               $diff = $slotDiffRenderer->getDiff( $oldContent, null );
+               $this->assertEquals( 'xxx|', $diff );
+       }
+
+       public function testAddModules() {
+               $output = $this->getMockBuilder( OutputPage::class )
+                       ->disableOriginalConstructor()
+                       ->setMethods( [ 'addModules' ] )
+                       ->getMock();
+               $output->expects( $this->once() )
+                       ->method( 'addModules' )
+                       ->with( 'foo' );
+               $differenceEngine = new CustomDifferenceEngine();
+               $slotDiffRenderer = new DifferenceEngineSlotDiffRenderer( $differenceEngine );
+               $slotDiffRenderer->addModules( $output );
+       }
+
+       public function testGetExtraCacheKeys() {
+               $differenceEngine = new CustomDifferenceEngine();
+               $slotDiffRenderer = new DifferenceEngineSlotDiffRenderer( $differenceEngine );
+               $extraCacheKeys = $slotDiffRenderer->getExtraCacheKeys();
+               $this->assertSame( [ 'foo' ], $extraCacheKeys );
+       }
+
+}
diff --git a/tests/phpunit/unit/includes/diff/SlotDiffRendererTest.php b/tests/phpunit/unit/includes/diff/SlotDiffRendererTest.php
new file mode 100644 (file)
index 0000000..a03280d
--- /dev/null
@@ -0,0 +1,78 @@
+<?php
+
+use Wikimedia\Assert\ParameterTypeException;
+use Wikimedia\TestingAccessWrapper;
+
+/**
+ * @covers SlotDiffRenderer
+ */
+class SlotDiffRendererTest extends \PHPUnit\Framework\TestCase {
+
+       /**
+        * @dataProvider provideNormalizeContents
+        */
+       public function testNormalizeContents(
+               $oldContent, $newContent, $allowedClasses,
+               $expectedOldContent, $expectedNewContent, $expectedExceptionClass
+       ) {
+               $slotDiffRenderer = $this->getMockBuilder( SlotDiffRenderer::class )
+                       ->getMock();
+               try {
+                       // __call needs help deciding which parameter to take by reference
+                       call_user_func_array( [ TestingAccessWrapper::newFromObject( $slotDiffRenderer ),
+                               'normalizeContents' ], [ &$oldContent, &$newContent, $allowedClasses ] );
+                       $this->assertEquals( $expectedOldContent, $oldContent );
+                       $this->assertEquals( $expectedNewContent, $newContent );
+               } catch ( Exception $e ) {
+                       if ( !$expectedExceptionClass ) {
+                               throw $e;
+                       }
+                       $this->assertInstanceOf( $expectedExceptionClass, $e );
+               }
+       }
+
+       public function provideNormalizeContents() {
+               return [
+                       'both null' => [ null, null, null, null, null, InvalidArgumentException::class ],
+                       'left null' => [
+                               null, new WikitextContent( 'abc' ), null,
+                               new WikitextContent( '' ), new WikitextContent( 'abc' ), null,
+                       ],
+                       'right null' => [
+                               new WikitextContent( 'def' ), null, null,
+                               new WikitextContent( 'def' ), new WikitextContent( '' ), null,
+                       ],
+                       'type filter' => [
+                               new WikitextContent( 'abc' ), new WikitextContent( 'def' ), WikitextContent::class,
+                               new WikitextContent( 'abc' ), new WikitextContent( 'def' ), null,
+                       ],
+                       'type filter (subclass)' => [
+                               new WikitextContent( 'abc' ), new WikitextContent( 'def' ), TextContent::class,
+                               new WikitextContent( 'abc' ), new WikitextContent( 'def' ), null,
+                       ],
+                       'type filter (null)' => [
+                               new WikitextContent( 'abc' ), null, TextContent::class,
+                               new WikitextContent( 'abc' ), new WikitextContent( '' ), null,
+                       ],
+                       'type filter failure (left)' => [
+                               new TextContent( 'abc' ), new WikitextContent( 'def' ), WikitextContent::class,
+                               null, null, ParameterTypeException::class,
+                       ],
+                       'type filter failure (right)' => [
+                               new WikitextContent( 'abc' ), new TextContent( 'def' ), WikitextContent::class,
+                               null, null, ParameterTypeException::class,
+                       ],
+                       'type filter (array syntax)' => [
+                               new WikitextContent( 'abc' ), new JsonContent( 'def' ),
+                               [ JsonContent::class, WikitextContent::class ],
+                               new WikitextContent( 'abc' ), new JsonContent( 'def' ), null,
+                       ],
+                       'type filter failure (array syntax)' => [
+                               new WikitextContent( 'abc' ), new CssContent( 'def' ),
+                               [ JsonContent::class, WikitextContent::class ],
+                               null, null, ParameterTypeException::class,
+                       ],
+               ];
+       }
+
+}
diff --git a/tests/phpunit/unit/includes/exception/HttpErrorTest.php b/tests/phpunit/unit/includes/exception/HttpErrorTest.php
new file mode 100644 (file)
index 0000000..c0f310a
--- /dev/null
@@ -0,0 +1,63 @@
+<?php
+
+/**
+ * @todo tests for HttpError::report
+ *
+ * @covers HttpError
+ */
+class HttpErrorTest extends \MediaWikiUnitTestCase {
+
+       public function testIsLoggable() {
+               $httpError = new HttpError( 500, 'server error!' );
+               $this->assertFalse( $httpError->isLoggable(), 'http error is not loggable' );
+       }
+
+       public function testGetStatusCode() {
+               $httpError = new HttpError( 500, 'server error!' );
+               $this->assertEquals( 500, $httpError->getStatusCode() );
+       }
+
+       /**
+        * @dataProvider getHtmlProvider
+        */
+       public function testGetHtml( array $expected, $content, $header ) {
+               $httpError = new HttpError( 500, $content, $header );
+               $errorHtml = $httpError->getHTML();
+
+               foreach ( $expected as $key => $html ) {
+                       $this->assertContains( $html, $errorHtml, $key );
+               }
+       }
+
+       public function getHtmlProvider() {
+               return [
+                       [
+                               [
+                                       'head html' => '<head><title>Server Error 123</title></head>',
+                                       'body html' => '<body><h1>Server Error 123</h1>'
+                                               . '<p>a server error!</p></body>'
+                               ],
+                               'a server error!',
+                               'Server Error 123'
+                       ],
+                       [
+                               [
+                                       'head html' => '<head><title>loginerror</title></head>',
+                                       'body html' => '<body><h1>loginerror</h1>'
+                                       . '<p>suspicious-userlogout</p></body>'
+                               ],
+                               new RawMessage( 'suspicious-userlogout' ),
+                               new RawMessage( 'loginerror' )
+                       ],
+                       [
+                               [
+                                       'head html' => '<html><head><title>Internal Server Error</title></head>',
+                                       'body html' => '<body><h1>Internal Server Error</h1>'
+                                               . '<p>a server error!</p></body></html>'
+                               ],
+                               'a server error!',
+                               null
+                       ]
+               ];
+       }
+}
diff --git a/tests/phpunit/unit/includes/exception/MWExceptionHandlerTest.php b/tests/phpunit/unit/includes/exception/MWExceptionHandlerTest.php
new file mode 100644 (file)
index 0000000..2b021c4
--- /dev/null
@@ -0,0 +1,74 @@
+<?php
+/**
+ * @author Antoine Musso
+ * @copyright Copyright © 2013, Antoine Musso
+ * @copyright Copyright © 2013, Wikimedia Foundation Inc.
+ * @file
+ */
+
+class MWExceptionHandlerTest extends \MediaWikiUnitTestCase {
+
+       /**
+        * @covers MWExceptionHandler::getRedactedTrace
+        */
+       public function testGetRedactedTrace() {
+               $refvar = 'value';
+               try {
+                       $array = [ 'a', 'b' ];
+                       $object = new stdClass();
+                       self::helperThrowAnException( $array, $object, $refvar );
+               } catch ( Exception $e ) {
+               }
+
+               # Make sure our stack trace contains an array and an object passed to
+               # some function in the stacktrace. Else, we can not assert the trace
+               # redaction achieved its job.
+               $trace = $e->getTrace();
+               $hasObject = false;
+               $hasArray = false;
+               foreach ( $trace as $frame ) {
+                       if ( !isset( $frame['args'] ) ) {
+                               continue;
+                       }
+                       foreach ( $frame['args'] as $arg ) {
+                               $hasObject = $hasObject || is_object( $arg );
+                               $hasArray = $hasArray || is_array( $arg );
+                       }
+
+                       if ( $hasObject && $hasArray ) {
+                               break;
+                       }
+               }
+               $this->assertTrue( $hasObject,
+                       "The stacktrace must have a function having an object has parameter" );
+               $this->assertTrue( $hasArray,
+                       "The stacktrace must have a function having an array has parameter" );
+
+               # Now we redact the trace.. and make sure no function arguments are
+               # arrays or objects.
+               $redacted = MWExceptionHandler::getRedactedTrace( $e );
+
+               foreach ( $redacted as $frame ) {
+                       if ( !isset( $frame['args'] ) ) {
+                               continue;
+                       }
+                       foreach ( $frame['args'] as $arg ) {
+                               $this->assertNotInternalType( 'array', $arg );
+                               $this->assertNotInternalType( 'object', $arg );
+                       }
+               }
+
+               $this->assertEquals( 'value', $refvar, 'Ensuring reference variable wasn\'t changed' );
+       }
+
+       /**
+        * Helper function for testExpandArgumentsInCall
+        *
+        * Pass it an object and an array, and something by reference :-)
+        *
+        * @throws Exception
+        */
+       protected static function helperThrowAnException( $a, $b, &$c ) {
+               throw new Exception();
+       }
+}
diff --git a/tests/phpunit/unit/includes/exception/ReadOnlyErrorTest.php b/tests/phpunit/unit/includes/exception/ReadOnlyErrorTest.php
new file mode 100644 (file)
index 0000000..c8460c9
--- /dev/null
@@ -0,0 +1,25 @@
+<?php
+
+/**
+ * @covers ReadOnlyError
+ * @author Addshore
+ */
+class ReadOnlyErrorTest extends \MediaWikiUnitTestCase {
+
+       protected function setUp() {
+               parent::setUp();
+
+               $loadBalancerMockFactory = function (): LoadBalancer {
+                       return $this->createMock( LoadBalancer::class );
+               };
+
+               $this->overrideMwServices( [ 'DBLoadBalancer' => $loadBalancerMockFactory ] );
+       }
+
+       public function testConstruction() {
+               $e = new ReadOnlyError();
+               $this->assertEquals( 'readonly', $e->title );
+               $this->assertEquals( 'readonlytext', $e->msg );
+               $this->assertEquals( wfReadOnlyReason() ?: [], $e->params );
+       }
+}
diff --git a/tests/phpunit/unit/includes/exception/UserNotLoggedInTest.php b/tests/phpunit/unit/includes/exception/UserNotLoggedInTest.php
new file mode 100644 (file)
index 0000000..3888c8e
--- /dev/null
@@ -0,0 +1,16 @@
+<?php
+
+/**
+ * @covers UserNotLoggedIn
+ * @author Addshore
+ */
+class UserNotLoggedInTest extends \MediaWikiUnitTestCase {
+
+       public function testConstruction() {
+               $e = new UserNotLoggedIn();
+               $this->assertEquals( 'exception-nologin', $e->title );
+               $this->assertEquals( 'exception-nologin-text', $e->msg );
+               $this->assertEquals( [], $e->params );
+       }
+
+}
diff --git a/tests/phpunit/unit/includes/externalstore/ExternalStoreFactoryTest.php b/tests/phpunit/unit/includes/externalstore/ExternalStoreFactoryTest.php
new file mode 100644 (file)
index 0000000..f762693
--- /dev/null
@@ -0,0 +1,41 @@
+<?php
+
+/**
+ * @covers ExternalStoreFactory
+ */
+class ExternalStoreFactoryTest extends PHPUnit\Framework\TestCase {
+
+       use MediaWikiCoversValidator;
+
+       public function testExternalStoreFactory_noStores() {
+               $factory = new ExternalStoreFactory( [] );
+               $this->assertFalse( $factory->getStoreObject( 'ForTesting' ) );
+               $this->assertFalse( $factory->getStoreObject( 'foo' ) );
+       }
+
+       public function provideStoreNames() {
+               yield 'Same case as construction' => [ 'ForTesting' ];
+               yield 'All lower case' => [ 'fortesting' ];
+               yield 'All upper case' => [ 'FORTESTING' ];
+               yield 'Mix of cases' => [ 'FOrTEsTInG' ];
+       }
+
+       /**
+        * @dataProvider provideStoreNames
+        */
+       public function testExternalStoreFactory_someStore_protoMatch( $proto ) {
+               $factory = new ExternalStoreFactory( [ 'ForTesting' ] );
+               $store = $factory->getStoreObject( $proto );
+               $this->assertInstanceOf( ExternalStoreForTesting::class, $store );
+       }
+
+       /**
+        * @dataProvider provideStoreNames
+        */
+       public function testExternalStoreFactory_someStore_noProtoMatch( $proto ) {
+               $factory = new ExternalStoreFactory( [ 'SomeOtherClassName' ] );
+               $store = $factory->getStoreObject( $proto );
+               $this->assertFalse( $store );
+       }
+
+}
diff --git a/tests/phpunit/unit/includes/filebackend/SwiftFileBackendTest.php b/tests/phpunit/unit/includes/filebackend/SwiftFileBackendTest.php
new file mode 100644 (file)
index 0000000..fbc1a57
--- /dev/null
@@ -0,0 +1,216 @@
+<?php
+
+use Wikimedia\TestingAccessWrapper;
+
+/**
+ * @group FileRepo
+ * @group FileBackend
+ * @group medium
+ *
+ * @covers SwiftFileBackend
+ * @covers SwiftFileBackendDirList
+ * @covers SwiftFileBackendFileList
+ * @covers SwiftFileBackendList
+ */
+class SwiftFileBackendTest extends \MediaWikiUnitTestCase {
+       /** @var TestingAccessWrapper Proxy to SwiftFileBackend */
+       private $backend;
+
+       protected function setUp() {
+               parent::setUp();
+
+               $this->backend = TestingAccessWrapper::newFromObject(
+                       new SwiftFileBackend( [
+                               'name'             => 'local-swift-testing',
+                               'class'            => SwiftFileBackend::class,
+                               'wikiId'           => 'unit-testing',
+                               'lockManager'      => LockManagerGroup::singleton()->get( 'fsLockManager' ),
+                               'swiftAuthUrl'     => 'http://127.0.0.1:8080/auth', // unused
+                               'swiftUser'        => 'test:tester',
+                               'swiftKey'         => 'testing',
+                               'swiftTempUrlKey'  => 'b3968d0207b54ece87cccc06515a89d4' // unused
+                       ] )
+               );
+       }
+
+       /**
+        * @dataProvider provider_testSanitizeHdrsStrict
+        */
+       public function testSanitizeHdrsStrict( $raw, $sanitized ) {
+               $hdrs = $this->backend->sanitizeHdrsStrict( [ 'headers' => $raw ] );
+
+               $this->assertEquals( $hdrs, $sanitized, 'sanitizeHdrsStrict() has expected result' );
+       }
+
+       public static function provider_testSanitizeHdrsStrict() {
+               return [
+                       [
+                               [
+                                       'content-length' => 345,
+                                       'content-type'   => 'image+bitmap/jpeg',
+                                       'content-disposition' => 'inline',
+                                       'content-duration' => 35.6363,
+                                       'content-Custom' => 'hello',
+                                       'x-content-custom' => 'hello'
+                               ],
+                               [
+                                       'content-disposition' => 'inline',
+                                       'content-duration' => 35.6363,
+                                       'content-custom' => 'hello',
+                                       'x-content-custom' => 'hello'
+                               ]
+                       ],
+                       [
+                               [
+                                       'content-length' => 345,
+                                       'content-type'   => 'image+bitmap/jpeg',
+                                       'content-Disposition' => 'inline; filename=xxx; ' . str_repeat( 'o', 1024 ),
+                                       'content-duration' => 35.6363,
+                                       'content-custom' => 'hello',
+                                       'x-content-custom' => 'hello'
+                               ],
+                               [
+                                       'content-disposition' => 'inline;filename=xxx',
+                                       'content-duration' => 35.6363,
+                                       'content-custom' => 'hello',
+                                       'x-content-custom' => 'hello'
+                               ]
+                       ],
+                       [
+                               [
+                                       'content-length' => 345,
+                                       'content-type'   => 'image+bitmap/jpeg',
+                                       'content-disposition' => 'filename=' . str_repeat( 'o', 1024 ) . ';inline',
+                                       'content-duration' => 35.6363,
+                                       'content-custom' => 'hello',
+                                       'x-content-custom' => 'hello'
+                               ],
+                               [
+                                       'content-disposition' => '',
+                                       'content-duration' => 35.6363,
+                                       'content-custom' => 'hello',
+                                       'x-content-custom' => 'hello'
+                               ]
+                       ]
+               ];
+       }
+
+       /**
+        * @dataProvider provider_testSanitizeHdrs
+        */
+       public function testSanitizeHdrs( $raw, $sanitized ) {
+               $hdrs = $this->backend->sanitizeHdrs( [ 'headers' => $raw ] );
+
+               $this->assertEquals( $hdrs, $sanitized, 'sanitizeHdrs() has expected result' );
+       }
+
+       public static function provider_testSanitizeHdrs() {
+               return [
+                       [
+                               [
+                                       'content-length' => 345,
+                                       'content-type'   => 'image+bitmap/jpeg',
+                                       'content-disposition' => 'inline',
+                                       'content-duration' => 35.6363,
+                                       'content-Custom' => 'hello',
+                                       'x-content-custom' => 'hello'
+                               ],
+                               [
+                                       'content-type'   => 'image+bitmap/jpeg',
+                                       'content-disposition' => 'inline',
+                                       'content-duration' => 35.6363,
+                                       'content-custom' => 'hello',
+                                       'x-content-custom' => 'hello'
+                               ]
+                       ],
+                       [
+                               [
+                                       'content-length' => 345,
+                                       'content-type'   => 'image+bitmap/jpeg',
+                                       'content-Disposition' => 'inline; filename=xxx; ' . str_repeat( 'o', 1024 ),
+                                       'content-duration' => 35.6363,
+                                       'content-custom' => 'hello',
+                                       'x-content-custom' => 'hello'
+                               ],
+                               [
+                                       'content-type'   => 'image+bitmap/jpeg',
+                                       'content-disposition' => 'inline;filename=xxx',
+                                       'content-duration' => 35.6363,
+                                       'content-custom' => 'hello',
+                                       'x-content-custom' => 'hello'
+                               ]
+                       ],
+                       [
+                               [
+                                       'content-length' => 345,
+                                       'content-type'   => 'image+bitmap/jpeg',
+                                       'content-disposition' => 'filename=' . str_repeat( 'o', 1024 ) . ';inline',
+                                       'content-duration' => 35.6363,
+                                       'content-custom' => 'hello',
+                                       'x-content-custom' => 'hello'
+                               ],
+                               [
+                                       'content-type'   => 'image+bitmap/jpeg',
+                                       'content-disposition' => '',
+                                       'content-duration' => 35.6363,
+                                       'content-custom' => 'hello',
+                                       'x-content-custom' => 'hello'
+                               ]
+                       ]
+               ];
+       }
+
+       /**
+        * @dataProvider provider_testGetMetadataHeaders
+        */
+       public function testGetMetadataHeaders( $raw, $sanitized ) {
+               $hdrs = $this->backend->getMetadataHeaders( $raw );
+
+               $this->assertEquals( $hdrs, $sanitized, 'getMetadataHeaders() has expected result' );
+       }
+
+       public static function provider_testGetMetadataHeaders() {
+               return [
+                       [
+                               [
+                                       'content-length' => 345,
+                                       'content-custom' => 'hello',
+                                       'x-content-custom' => 'hello',
+                                       'x-object-meta-custom' => 5,
+                                       'x-object-meta-sha1Base36' => 'a3deadfg...',
+                               ],
+                               [
+                                       'x-object-meta-custom' => 5,
+                                       'x-object-meta-sha1base36' => 'a3deadfg...',
+                               ]
+                       ]
+               ];
+       }
+
+       /**
+        * @dataProvider provider_testGetMetadata
+        */
+       public function testGetMetadata( $raw, $sanitized ) {
+               $hdrs = $this->backend->getMetadata( $raw );
+
+               $this->assertEquals( $hdrs, $sanitized, 'getMetadata() has expected result' );
+       }
+
+       public static function provider_testGetMetadata() {
+               return [
+                       [
+                               [
+                                       'content-length' => 345,
+                                       'content-custom' => 'hello',
+                                       'x-content-custom' => 'hello',
+                                       'x-object-meta-custom' => 5,
+                                       'x-object-meta-sha1Base36' => 'a3deadfg...',
+                               ],
+                               [
+                                       'custom' => 5,
+                                       'sha1base36' => 'a3deadfg...',
+                               ]
+                       ]
+               ];
+       }
+}
diff --git a/tests/phpunit/unit/includes/filerepo/FileBackendDBRepoWrapperTest.php b/tests/phpunit/unit/includes/filerepo/FileBackendDBRepoWrapperTest.php
new file mode 100644 (file)
index 0000000..4db9892
--- /dev/null
@@ -0,0 +1,140 @@
+<?php
+
+class FileBackendDBRepoWrapperTest extends \MediaWikiUnitTestCase {
+       protected $backendName = 'foo-backend';
+       protected $repoName = 'pureTestRepo';
+
+       /**
+        * @dataProvider getBackendPathsProvider
+        * @covers FileBackendDBRepoWrapper::getBackendPaths
+        */
+       public function testGetBackendPaths(
+               $mocks,
+               $latest,
+               $dbReadsExpected,
+               $dbReturnValue,
+               $originalPath,
+               $expectedBackendPath,
+               $message ) {
+               list( $dbMock, $backendMock, $wrapperMock ) = $mocks;
+
+               $dbMock->expects( $dbReadsExpected )
+                       ->method( 'selectField' )
+                       ->will( $this->returnValue( $dbReturnValue ) );
+
+               $newPaths = $wrapperMock->getBackendPaths( [ $originalPath ], $latest );
+
+               $this->assertEquals(
+                       $expectedBackendPath,
+                       $newPaths[0],
+                       $message );
+       }
+
+       public function getBackendPathsProvider() {
+               $prefix = 'mwstore://' . $this->backendName . '/' . $this->repoName;
+               $mocksForCaching = $this->getMocks();
+
+               return [
+                       [
+                               $mocksForCaching,
+                               false,
+                               $this->once(),
+                               '96246614d75ba1703bdfd5d7660bb57407aaf5d9',
+                               $prefix . '-public/f/o/foobar.jpg',
+                               $prefix . '-original/9/6/2/96246614d75ba1703bdfd5d7660bb57407aaf5d9',
+                               'Public path translated correctly',
+                       ],
+                       [
+                               $mocksForCaching,
+                               false,
+                               $this->never(),
+                               '96246614d75ba1703bdfd5d7660bb57407aaf5d9',
+                               $prefix . '-public/f/o/foobar.jpg',
+                               $prefix . '-original/9/6/2/96246614d75ba1703bdfd5d7660bb57407aaf5d9',
+                               'LRU cache leveraged',
+                       ],
+                       [
+                               $this->getMocks(),
+                               true,
+                               $this->once(),
+                               '96246614d75ba1703bdfd5d7660bb57407aaf5d9',
+                               $prefix . '-public/f/o/foobar.jpg',
+                               $prefix . '-original/9/6/2/96246614d75ba1703bdfd5d7660bb57407aaf5d9',
+                               'Latest obtained',
+                       ],
+                       [
+                               $this->getMocks(),
+                               true,
+                               $this->never(),
+                               '96246614d75ba1703bdfd5d7660bb57407aaf5d9',
+                               $prefix . '-deleted/f/o/foobar.jpg',
+                               $prefix . '-original/f/o/o/foobar',
+                               'Deleted path translated correctly',
+                       ],
+                       [
+                               $this->getMocks(),
+                               true,
+                               $this->once(),
+                               null,
+                               $prefix . '-public/b/a/baz.jpg',
+                               $prefix . '-public/b/a/baz.jpg',
+                               'Path left untouched if no sha1 can be found',
+                       ],
+               ];
+       }
+
+       /**
+        * @covers FileBackendDBRepoWrapper::getFileContentsMulti
+        */
+       public function testGetFileContentsMulti() {
+               list( $dbMock, $backendMock, $wrapperMock ) = $this->getMocks();
+
+               $sha1Path = 'mwstore://' . $this->backendName . '/' . $this->repoName
+                       . '-original/9/6/2/96246614d75ba1703bdfd5d7660bb57407aaf5d9';
+               $filenamePath = 'mwstore://' . $this->backendName . '/' . $this->repoName
+                       . '-public/f/o/foobar.jpg';
+
+               $dbMock->expects( $this->once() )
+                       ->method( 'selectField' )
+                       ->will( $this->returnValue( '96246614d75ba1703bdfd5d7660bb57407aaf5d9' ) );
+
+               $backendMock->expects( $this->once() )
+                       ->method( 'getFileContentsMulti' )
+                       ->will( $this->returnValue( [ $sha1Path => 'foo' ] ) );
+
+               $result = $wrapperMock->getFileContentsMulti( [ 'srcs' => [ $filenamePath ] ] );
+
+               $this->assertEquals(
+                       [ $filenamePath => 'foo' ],
+                       $result,
+                       'File contents paths translated properly'
+               );
+       }
+
+       protected function getMocks() {
+               $dbMock = $this->getMockBuilder( Wikimedia\Rdbms\IDatabase::class )
+                       ->disableOriginalClone()
+                       ->disableOriginalConstructor()
+                       ->getMock();
+
+               $backendMock = $this->getMockBuilder( FSFileBackend::class )
+                       ->setConstructorArgs( [ [
+                                       'name' => $this->backendName,
+                                       'wikiId' => wfWikiID()
+                               ] ] )
+                       ->getMock();
+
+               $wrapperMock = $this->getMockBuilder( FileBackendDBRepoWrapper::class )
+                       ->setMethods( [ 'getDB' ] )
+                       ->setConstructorArgs( [ [
+                                       'backend' => $backendMock,
+                                       'repoName' => $this->repoName,
+                                       'dbHandleFactory' => null
+                               ] ] )
+                       ->getMock();
+
+               $wrapperMock->expects( $this->any() )->method( 'getDB' )->will( $this->returnValue( $dbMock ) );
+
+               return [ $dbMock, $backendMock, $wrapperMock ];
+       }
+}
diff --git a/tests/phpunit/unit/includes/filerepo/FileRepoTest.php b/tests/phpunit/unit/includes/filerepo/FileRepoTest.php
new file mode 100644 (file)
index 0000000..90cd5ec
--- /dev/null
@@ -0,0 +1,55 @@
+<?php
+
+class FileRepoTest extends \MediaWikiUnitTestCase {
+
+       /**
+        * @expectedException MWException
+        * @covers FileRepo::__construct
+        */
+       public function testFileRepoConstructionOptionCanNotBeNull() {
+               new FileRepo();
+       }
+
+       /**
+        * @expectedException MWException
+        * @covers FileRepo::__construct
+        */
+       public function testFileRepoConstructionOptionCanNotBeAnEmptyArray() {
+               new FileRepo( [] );
+       }
+
+       /**
+        * @expectedException MWException
+        * @covers FileRepo::__construct
+        */
+       public function testFileRepoConstructionOptionNeedNameKey() {
+               new FileRepo( [
+                       'backend' => 'foobar'
+               ] );
+       }
+
+       /**
+        * @expectedException MWException
+        * @covers FileRepo::__construct
+        */
+       public function testFileRepoConstructionOptionNeedBackendKey() {
+               new FileRepo( [
+                       'name' => 'foobar'
+               ] );
+       }
+
+       /**
+        * @covers FileRepo::__construct
+        */
+       public function testFileRepoConstructionWithRequiredOptions() {
+               $f = new FileRepo( [
+                       'name' => 'FileRepoTestRepository',
+                       'backend' => new FSFileBackend( [
+                               'name' => 'local-testing',
+                               'wikiId' => 'test_wiki',
+                               'containerPaths' => []
+                       ] )
+               ] );
+               $this->assertInstanceOf( FileRepo::class, $f );
+       }
+}
diff --git a/tests/phpunit/unit/includes/htmlform/HTMLAutoCompleteSelectFieldTest.php b/tests/phpunit/unit/includes/htmlform/HTMLAutoCompleteSelectFieldTest.php
new file mode 100644 (file)
index 0000000..64f8a00
--- /dev/null
@@ -0,0 +1,66 @@
+<?php
+/**
+ * @covers HTMLAutoCompleteSelectField
+ */
+class HTMLAutoCompleteSelectFieldTest extends \MediaWikiUnitTestCase {
+
+       public $options = [
+               'Bulgaria'     => 'BGR',
+               'Burkina Faso' => 'BFA',
+               'Burundi'      => 'BDI',
+       ];
+
+       /**
+        * Verify that attempting to instantiate an HTMLAutoCompleteSelectField
+        * without providing any autocomplete options causes an exception to be
+        * thrown.
+        *
+        * @expectedException        MWException
+        * @expectedExceptionMessage called without any autocompletions
+        */
+       function testMissingAutocompletions() {
+               new HTMLAutoCompleteSelectField( [ 'fieldname' => 'Test' ] );
+       }
+
+       /**
+        * Verify that the autocomplete options are correctly encoded as
+        * the 'data-autocomplete' attribute of the field.
+        *
+        * @covers HTMLAutoCompleteSelectField::getAttributes
+        */
+       function testGetAttributes() {
+               $field = new HTMLAutoCompleteSelectField( [
+                       'fieldname'    => 'Test',
+                       'autocomplete' => $this->options,
+               ] );
+
+               $attributes = $field->getAttributes( [] );
+               $this->assertEquals( array_keys( $this->options ),
+                       FormatJson::decode( $attributes['data-autocomplete'] ),
+                       "The 'data-autocomplete' attribute encodes autocomplete option keys as a JSON array."
+               );
+       }
+
+       /**
+        * Test that the optional select dropdown is included or excluded based on
+        * the presence or absence of the 'options' parameter.
+        */
+       function testOptionalSelectElement() {
+               $params = [
+                       'fieldname'         => 'Test',
+                       'autocomplete-data' => $this->options,
+                       'options'           => $this->options,
+               ];
+
+               $field = new HTMLAutoCompleteSelectField( $params );
+               $html = $field->getInputHTML( false );
+               $this->assertRegExp( '/select/', $html,
+                       "When the 'options' parameter is set, the HTML includes a <select>" );
+
+               unset( $params['options'] );
+               $field = new HTMLAutoCompleteSelectField( $params );
+               $html = $field->getInputHTML( false );
+               $this->assertNotRegExp( '/select/', $html,
+                       "When the 'options' parameter is not set, the HTML does not include a <select>" );
+       }
+}
diff --git a/tests/phpunit/unit/includes/htmlform/HTMLCheckMatrixTest.php b/tests/phpunit/unit/includes/htmlform/HTMLCheckMatrixTest.php
new file mode 100644 (file)
index 0000000..9c41ab8
--- /dev/null
@@ -0,0 +1,104 @@
+<?php
+
+/**
+ * @covers HTMLCheckMatrix
+ */
+class HTMLCheckMatrixTest extends \MediaWikiUnitTestCase {
+       private static $defaultOptions = [
+               'rows' => [ 'r1', 'r2' ],
+               'columns' => [ 'c1', 'c2' ],
+               'fieldname' => 'test',
+       ];
+
+       public function testPlainInstantiation() {
+               try {
+                       new HTMLCheckMatrix( [] );
+               } catch ( MWException $e ) {
+                       $this->assertInstanceOf( HTMLFormFieldRequiredOptionsException::class, $e );
+                       return;
+               }
+
+               $this->fail( 'Expected MWException indicating missing parameters but none was thrown.' );
+       }
+
+       public function testInstantiationWithMinimumRequiredParameters() {
+               new HTMLCheckMatrix( self::$defaultOptions );
+               $this->assertTrue( true ); // form instantiation must throw exception on failure
+       }
+
+       public function testValidateCallsUserDefinedValidationCallback() {
+               $called = false;
+               $field = new HTMLCheckMatrix( self::$defaultOptions + [
+                       'validation-callback' => function () use ( &$called ) {
+                               $called = true;
+
+                               return false;
+                       },
+               ] );
+               $this->assertEquals( false, $this->validate( $field, [] ) );
+               $this->assertTrue( $called );
+       }
+
+       public function testValidateRequiresArrayInput() {
+               $field = new HTMLCheckMatrix( self::$defaultOptions );
+               $this->assertEquals( false, $this->validate( $field, null ) );
+               $this->assertEquals( false, $this->validate( $field, true ) );
+               $this->assertEquals( false, $this->validate( $field, 'abc' ) );
+               $this->assertEquals( false, $this->validate( $field, new stdClass ) );
+               $this->assertEquals( true, $this->validate( $field, [] ) );
+       }
+
+       public function testValidateAllowsOnlyKnownTags() {
+               $field = new HTMLCheckMatrix( self::$defaultOptions );
+               $this->assertInstanceOf( Message::class, $this->validate( $field, [ 'foo' ] ) );
+       }
+
+       public function testValidateAcceptsPartialTagList() {
+               $field = new HTMLCheckMatrix( self::$defaultOptions );
+               $this->assertTrue( $this->validate( $field, [] ) );
+               $this->assertTrue( $this->validate( $field, [ 'c1-r1' ] ) );
+               $this->assertTrue( $this->validate( $field, [ 'c1-r1', 'c1-r2', 'c2-r1', 'c2-r2' ] ) );
+       }
+
+       /**
+        * This form object actually has no visibility into what happens later on, but essentially
+        * if the data submitted by the user passes validate the following is run:
+        * foreach ( $field->filterDataForSubmit( $data ) as $k => $v ) {
+        *     $user->setOption( $k, $v );
+        * }
+        */
+       public function testValuesForcedOnRemainOn() {
+               $field = new HTMLCheckMatrix( self::$defaultOptions + [
+                               'force-options-on' => [ 'c2-r1' ],
+                       ] );
+               $expected = [
+                       'c1-r1' => false,
+                       'c1-r2' => false,
+                       'c2-r1' => true,
+                       'c2-r2' => false,
+               ];
+               $this->assertEquals( $expected, $field->filterDataForSubmit( [] ) );
+       }
+
+       public function testValuesForcedOffRemainOff() {
+               $field = new HTMLCheckMatrix( self::$defaultOptions + [
+                               'force-options-off' => [ 'c1-r2', 'c2-r2' ],
+                       ] );
+               $expected = [
+                       'c1-r1' => true,
+                       'c1-r2' => false,
+                       'c2-r1' => true,
+                       'c2-r2' => false,
+               ];
+               // array_keys on the result simulates submitting all fields checked
+               $this->assertEquals( $expected, $field->filterDataForSubmit( array_keys( $expected ) ) );
+       }
+
+       protected function validate( HTMLFormField $field, $submitted ) {
+               return $field->validate(
+                       $submitted,
+                       [ self::$defaultOptions['fieldname'] => $submitted ]
+               );
+       }
+
+}
diff --git a/tests/phpunit/unit/includes/htmlform/HTMLFormTest.php b/tests/phpunit/unit/includes/htmlform/HTMLFormTest.php
new file mode 100644 (file)
index 0000000..d9d2cb1
--- /dev/null
@@ -0,0 +1,63 @@
+<?php
+
+/**
+ * @covers HTMLForm
+ *
+ * @license GPL-2.0-or-later
+ * @author Gergő Tisza
+ */
+class HTMLFormTest extends \MediaWikiUnitTestCase {
+
+       private function newInstance() {
+               $form = new HTMLForm( [] );
+               $form->setTitle( Title::newFromText( 'Foo' ) );
+               return $form;
+       }
+
+       public function testGetHTML_empty() {
+               $form = $this->newInstance();
+               $form->prepareForm();
+               $html = $form->getHTML( false );
+               $this->assertStringStartsWith( '<form ', $html );
+       }
+
+       /**
+        * @expectedException LogicException
+        */
+       public function testGetHTML_noPrepare() {
+               $form = $this->newInstance();
+               $form->getHTML( false );
+       }
+
+       public function testAutocompleteDefaultsToNull() {
+               $form = $this->newInstance();
+               $this->assertNotContains( 'autocomplete', $form->wrapForm( '' ) );
+       }
+
+       public function testAutocompleteWhenSetToNull() {
+               $form = $this->newInstance();
+               $form->setAutocomplete( null );
+               $this->assertNotContains( 'autocomplete', $form->wrapForm( '' ) );
+       }
+
+       public function testAutocompleteWhenSetToFalse() {
+               $form = $this->newInstance();
+               // Previously false was used instead of null to indicate the attribute should not be set
+               $form->setAutocomplete( false );
+               $this->assertNotContains( 'autocomplete', $form->wrapForm( '' ) );
+       }
+
+       public function testAutocompleteWhenSetToOff() {
+               $form = $this->newInstance();
+               $form->setAutocomplete( 'off' );
+               $this->assertContains( ' autocomplete="off"', $form->wrapForm( '' ) );
+       }
+
+       public function testGetPreText() {
+               $preText = 'TEST';
+               $form = $this->newInstance();
+               $form->setPreText( $preText );
+               $this->assertSame( $preText, $form->getPreText() );
+       }
+
+}
diff --git a/tests/phpunit/unit/includes/htmlform/HTMLRestrictionsFieldTest.php b/tests/phpunit/unit/includes/htmlform/HTMLRestrictionsFieldTest.php
new file mode 100644 (file)
index 0000000..c4290e1
--- /dev/null
@@ -0,0 +1,71 @@
+<?php
+
+/**
+ * @covers HTMLRestrictionsField
+ */
+class HTMLRestrictionsFieldTest extends PHPUnit\Framework\TestCase {
+
+       use MediaWikiCoversValidator;
+
+       public function testConstruct() {
+               $field = new HTMLRestrictionsField( [ 'fieldname' => 'restrictions' ] );
+               $this->assertNotEmpty( $field->getLabel(), 'has a default label' );
+               $this->assertNotEmpty( $field->getHelpText(), 'has a default help text' );
+               $this->assertEquals( MWRestrictions::newDefault(), $field->getDefault(),
+                       'defaults to the default MWRestrictions object' );
+
+               $field = new HTMLRestrictionsField( [
+                       'fieldname' => 'restrictions',
+                       'label' => 'foo',
+                       'help' => 'bar',
+                       'default' => 'baz',
+               ] );
+               $this->assertEquals( 'foo', $field->getLabel(), 'label can be customized' );
+               $this->assertEquals( 'bar', $field->getHelpText(), 'help text can be customized' );
+               $this->assertEquals( 'baz', $field->getDefault(), 'default can be customized' );
+       }
+
+       /**
+        * @dataProvider provideValidate
+        */
+       public function testForm( $text, $value ) {
+               $form = HTMLForm::factory( 'ooui', [
+                       'restrictions' => [ 'class' => HTMLRestrictionsField::class ],
+               ] );
+               $request = new FauxRequest( [ 'wprestrictions' => $text ], true );
+               $context = new DerivativeContext( RequestContext::getMain() );
+               $context->setRequest( $request );
+               $form->setContext( $context );
+               $form->setTitle( Title::newFromText( 'Main Page' ) )->setSubmitCallback( function () {
+                       return true;
+               } )->prepareForm();
+               $status = $form->trySubmit();
+
+               if ( $status instanceof StatusValue ) {
+                       $this->assertEquals( $value !== false, $status->isGood() );
+               } elseif ( $value === false ) {
+                       $this->assertNotSame( true, $status );
+               } else {
+                       $this->assertSame( true, $status );
+               }
+
+               if ( $value !== false ) {
+                       $restrictions = $form->mFieldData['restrictions'];
+                       $this->assertInstanceOf( MWRestrictions::class, $restrictions );
+                       $this->assertEquals( $value, $restrictions->toArray()['IPAddresses'] );
+               }
+
+               // sanity
+               $form->getHTML( $status );
+       }
+
+       public function provideValidate() {
+               return [
+                       // submitted text, value of 'IPAddresses' key or false for validation error
+                       [ null, [ '0.0.0.0/0', '::/0' ] ],
+                       [ '', [] ],
+                       [ "1.2.3.4\n::/0", [ '1.2.3.4', '::/0' ] ],
+                       [ "1.2.3.4\n::/x", false ],
+               ];
+       }
+}
diff --git a/tests/phpunit/unit/includes/http/GuzzleHttpRequestTest.php b/tests/phpunit/unit/includes/http/GuzzleHttpRequestTest.php
new file mode 100644 (file)
index 0000000..e271ac6
--- /dev/null
@@ -0,0 +1,151 @@
+<?php
+
+use GuzzleHttp\Handler\MockHandler;
+use GuzzleHttp\HandlerStack;
+use GuzzleHttp\Psr7\Response;
+use GuzzleHttp\Psr7\Request;
+
+/**
+ * class for tests of GuzzleHttpRequest
+ *
+ * No actual requests are made herein - all external communications are mocked
+ *
+ * @covers GuzzleHttpRequest
+ * @covers MWHttpRequest
+ */
+class GuzzleHttpRequestTest extends \MediaWikiUnitTestCase {
+       /**
+        * Placeholder url to use for various tests.  This is never contacted, but we must use
+        * a url of valid format to avoid validation errors.
+        * @var string
+        */
+       protected $exampleUrl = 'http://www.example.test';
+
+       /**
+        * Minimal example body text
+        * @var string
+        */
+       protected $exampleBodyText = 'x';
+
+       /**
+        * For accumulating callback data for testing
+        * @var string
+        */
+       protected $bodyTextReceived = '';
+
+       /**
+        * Callback: process a chunk of the result of a HTTP request
+        *
+        * @param mixed $req
+        * @param string $buffer
+        * @return int Number of bytes handled
+        */
+       public function processHttpDataChunk( $req, $buffer ) {
+               $this->bodyTextReceived .= $buffer;
+               return strlen( $buffer );
+       }
+
+       public function testSuccess() {
+               $handler = HandlerStack::create( new MockHandler( [ new Response( 200, [
+                       'status' => 200,
+               ], $this->exampleBodyText ) ] ) );
+               $r = new GuzzleHttpRequest( $this->exampleUrl, [ 'handler' => $handler ] );
+               $r->execute();
+
+               $this->assertEquals( 200, $r->getStatus() );
+               $this->assertEquals( $this->exampleBodyText, $r->getContent() );
+       }
+
+       public function testSuccessConstructorCallback() {
+               $this->bodyTextReceived = '';
+               $handler = HandlerStack::create( new MockHandler( [ new Response( 200, [
+                       'status' => 200,
+               ], $this->exampleBodyText ) ] ) );
+               $r = new GuzzleHttpRequest( $this->exampleUrl, [
+                       'callback' => [ $this, 'processHttpDataChunk' ],
+                       'handler' => $handler,
+               ] );
+               $r->execute();
+
+               $this->assertEquals( 200, $r->getStatus() );
+               $this->assertEquals( $this->exampleBodyText, $this->bodyTextReceived );
+       }
+
+       public function testSuccessSetCallback() {
+               $this->bodyTextReceived = '';
+               $handler = HandlerStack::create( new MockHandler( [ new Response( 200, [
+                       'status' => 200,
+               ], $this->exampleBodyText ) ] ) );
+               $r = new GuzzleHttpRequest( $this->exampleUrl, [
+                       'handler' => $handler,
+               ] );
+               $r->setCallback( [ $this, 'processHttpDataChunk' ] );
+               $r->execute();
+
+               $this->assertEquals( 200, $r->getStatus() );
+               $this->assertEquals( $this->exampleBodyText, $this->bodyTextReceived );
+       }
+
+       /**
+        * use a callback stream to pipe the mocked response data to our callback function
+        */
+       public function testSuccessSink() {
+               $this->bodyTextReceived = '';
+               $handler = HandlerStack::create( new MockHandler( [ new Response( 200, [
+                       'status' => 200,
+               ], $this->exampleBodyText ) ] ) );
+               $r = new GuzzleHttpRequest( $this->exampleUrl, [
+                       'handler' => $handler,
+                       'sink' => new MWCallbackStream( [ $this, 'processHttpDataChunk' ] ),
+               ] );
+               $r->execute();
+
+               $this->assertEquals( 200, $r->getStatus() );
+               $this->assertEquals( $this->exampleBodyText, $this->bodyTextReceived );
+       }
+
+       public function testBadUrl() {
+               $r = new GuzzleHttpRequest( '' );
+               $s = $r->execute();
+               $errorMsg = $s->getErrorsByType( 'error' )[0]['message'];
+
+               $this->assertEquals( 0, $r->getStatus() );
+               $this->assertEquals( 'http-invalid-url', $errorMsg );
+       }
+
+       public function testConnectException() {
+               $handler = HandlerStack::create( new MockHandler( [ new GuzzleHttp\Exception\ConnectException(
+                       'Mock Connection Exception', new Request( 'GET', $this->exampleUrl )
+               ) ] ) );
+               $r = new GuzzleHttpRequest( $this->exampleUrl, [ 'handler' => $handler ] );
+               $s = $r->execute();
+               $errorMsg = $s->getErrorsByType( 'error' )[0]['message'];
+
+               $this->assertEquals( 0, $r->getStatus() );
+               $this->assertEquals( 'http-request-error', $errorMsg );
+       }
+
+       public function testTimeout() {
+               $handler = HandlerStack::create( new MockHandler( [ new GuzzleHttp\Exception\RequestException(
+                       'Connection timed out', new Request( 'GET', $this->exampleUrl )
+               ) ] ) );
+               $r = new GuzzleHttpRequest( $this->exampleUrl, [ 'handler' => $handler ] );
+               $s = $r->execute();
+               $errorMsg = $s->getErrorsByType( 'error' )[0]['message'];
+
+               $this->assertEquals( 0, $r->getStatus() );
+               $this->assertEquals( 'http-timed-out', $errorMsg );
+       }
+
+       public function testNotFound() {
+               $handler = HandlerStack::create( new MockHandler( [ new Response( 404, [
+                       'status' => '404',
+               ] ) ] ) );
+               $r = new GuzzleHttpRequest( $this->exampleUrl, [ 'handler' => $handler ] );
+               $s = $r->execute();
+               $errorMsg = $s->getErrorsByType( 'error' )[0]['message'];
+
+               $this->assertEquals( 404, $r->getStatus() );
+               $this->assertEquals( 'http-bad-status', $errorMsg );
+       }
+}
diff --git a/tests/phpunit/unit/includes/http/HttpRequestFactoryTest.php b/tests/phpunit/unit/includes/http/HttpRequestFactoryTest.php
new file mode 100644 (file)
index 0000000..61c67fd
--- /dev/null
@@ -0,0 +1,119 @@
+<?php
+
+use MediaWiki\Http\HttpRequestFactory;
+
+/**
+ * @covers MediaWiki\Http\HttpRequestFactory
+ */
+class HttpRequestFactoryTest extends \MediaWikiUnitTestCase {
+
+       /**
+        * @return HttpRequestFactory
+        */
+       private function newFactory() {
+               return new HttpRequestFactory();
+       }
+
+       /**
+        * @return HttpRequestFactory
+        */
+       private function newFactoryWithFakeRequest(
+               MWHttpRequest $req,
+               $expectedUrl,
+               $expectedOptions = []
+       ) {
+               $factory = $this->getMockBuilder( HttpRequestFactory::class )
+                       ->setMethods( [ 'create' ] )
+                       ->getMock();
+
+               $factory->method( 'create' )
+                       ->willReturnCallback(
+                               function ( $url, array $options = [], $caller = __METHOD__ )
+                                       use ( $req, $expectedUrl, $expectedOptions )
+                               {
+                                       $this->assertSame( $url, $expectedUrl );
+
+                                       foreach ( $expectedOptions as $opt => $exp ) {
+                                               $this->assertArrayHasKey( $opt, $options );
+                                               $this->assertSame( $exp, $options[$opt] );
+                                       }
+
+                                       return $req;
+                               }
+                       );
+
+               return $factory;
+       }
+
+       /**
+        * @return MWHttpRequest
+        */
+       private function newFakeRequest( $result ) {
+               $req = $this->getMockBuilder( MWHttpRequest::class )
+                       ->disableOriginalConstructor()
+                       ->setMethods( [ 'getContent', 'execute' ] )
+                       ->getMock();
+
+               if ( $result instanceof Status ) {
+                       $req->method( 'getContent' )
+                               ->willReturn( $result->getValue() );
+                       $req->method( 'execute' )
+                               ->willReturn( $result );
+               } else {
+                       $req->method( 'getContent' )
+                               ->willReturn( $result );
+                       $req->method( 'execute' )
+                               ->willReturn( Status::newGood( $result ) );
+               }
+
+               return $req;
+       }
+
+       public function testCreate() {
+               $factory = $this->newFactory();
+               $this->assertInstanceOf( 'MWHttpRequest', $factory->create( 'http://example.test' ) );
+       }
+
+       public function testGetUserAgent() {
+               $factory = $this->newFactory();
+               $this->assertStringStartsWith( 'MediaWiki/', $factory->getUserAgent() );
+       }
+
+       public function testGet() {
+               $req = $this->newFakeRequest( __METHOD__ );
+               $factory = $this->newFactoryWithFakeRequest(
+                       $req, 'https://example.test', [ 'method' => 'GET' ]
+               );
+
+               $this->assertSame( __METHOD__, $factory->get( 'https://example.test' ) );
+       }
+
+       public function testPost() {
+               $req = $this->newFakeRequest( __METHOD__ );
+               $factory = $this->newFactoryWithFakeRequest(
+                       $req, 'https://example.test', [ 'method' => 'POST' ]
+               );
+
+               $this->assertSame( __METHOD__, $factory->post( 'https://example.test' ) );
+       }
+
+       public function testRequest() {
+               $req = $this->newFakeRequest( __METHOD__ );
+               $factory = $this->newFactoryWithFakeRequest(
+                       $req, 'https://example.test', [ 'method' => 'GET' ]
+               );
+
+               $this->assertSame( __METHOD__, $factory->request( 'GET', 'https://example.test' ) );
+       }
+
+       public function testRequest_failed() {
+               $status = Status::newFatal( 'testing' );
+               $req = $this->newFakeRequest( $status );
+               $factory = $this->newFactoryWithFakeRequest(
+                       $req, 'https://example.test', [ 'method' => 'POST' ]
+               );
+
+               $this->assertNull( $factory->request( 'POST', 'https://example.test' ) );
+       }
+
+}
diff --git a/tests/phpunit/unit/includes/installer/InstallDocFormatterTest.php b/tests/phpunit/unit/includes/installer/InstallDocFormatterTest.php
new file mode 100644 (file)
index 0000000..fddc3b8
--- /dev/null
@@ -0,0 +1,83 @@
+<?php
+
+class InstallDocFormatterTest extends \MediaWikiUnitTestCase {
+       /**
+        * @covers InstallDocFormatter
+        * @dataProvider provideDocFormattingTests
+        */
+       public function testFormat( $expected, $unformattedText, $message = '' ) {
+               $this->assertEquals(
+                       $expected,
+                       InstallDocFormatter::format( $unformattedText ),
+                       $message
+               );
+       }
+
+       /**
+        * Provider for testFormat()
+        */
+       public static function provideDocFormattingTests() {
+               # Format: (expected string, unformattedText string, optional message)
+               return [
+                       # Escape some wikitext
+                       [ 'Install &lt;tag>', 'Install <tag>', 'Escaping <' ],
+                       [ 'Install &#123;&#123;template}}', 'Install {{template}}', 'Escaping [[' ],
+                       [ 'Install &#91;&#91;page]]', 'Install [[page]]', 'Escaping {{' ],
+                       [ 'Install &#95;&#95;TOC&#95;&#95;', 'Install __TOC__', 'Escaping __' ],
+                       [ 'Install ', "Install \r", 'Removing \r' ],
+
+                       # Transform \t{1,2} into :{1,2}
+                       [ ':One indentation', "\tOne indentation", 'Replacing a single \t' ],
+                       [ '::Two indentations', "\t\tTwo indentations", 'Replacing 2 x \t' ],
+
+                       # Transform 'T123' links
+                       [
+                               '<span class="config-plainlink">[https://phabricator.wikimedia.org/T123 T123]</span>',
+                               'T123', 'Testing T123 links' ],
+                       [
+                               'bug <span class="config-plainlink">[https://phabricator.wikimedia.org/T123 T123]</span>',
+                               'bug T123', 'Testing bug T123 links' ],
+                       [
+                               '(<span class="config-plainlink">[https://phabricator.wikimedia.org/T987654 T987654]</span>)',
+                               '(T987654)', 'Testing (T987654) links' ],
+
+                       # "Tabc" shouldn't work
+                       [ 'Tfoobar', 'Tfoobar', "Don't match T followed by non-digits" ],
+                       [ 'T!!fakefake!!', 'T!!fakefake!!', "Don't match T followed by non-digits" ],
+
+                       # Transform 'bug 123' links
+                       [
+                               '<span class="config-plainlink">[https://bugzilla.wikimedia.org/123 bug 123]</span>',
+                               'bug 123', 'Testing bug 123 links' ],
+                       [
+                               '(<span class="config-plainlink">[https://bugzilla.wikimedia.org/987654 bug 987654]</span>)',
+                               '(bug 987654)', 'Testing (bug 987654) links' ],
+
+                       # "bug abc" shouldn't work
+                       [ 'bug foobar', 'bug foobar', "Don't match bug followed by non-digits" ],
+                       [ 'bug !!fakefake!!', 'bug !!fakefake!!', "Don't match bug followed by non-digits" ],
+
+                       # Transform '$wgFooBar' links
+                       [
+                               '<span class="config-plainlink">'
+                                       . '[https://www.mediawiki.org/wiki/Manual:$wgFooBar $wgFooBar]</span>',
+                               '$wgFooBar', 'Testing basic $wgFooBar' ],
+                       [
+                               '<span class="config-plainlink">'
+                                       . '[https://www.mediawiki.org/wiki/Manual:$wgFooBar45 $wgFooBar45]</span>',
+                               '$wgFooBar45', 'Testing $wgFooBar45 (with numbers)' ],
+                       [
+                               '<span class="config-plainlink">'
+                                       . '[https://www.mediawiki.org/wiki/Manual:$wgFoo_Bar $wgFoo_Bar]</span>',
+                               '$wgFoo_Bar', 'Testing $wgFoo_Bar (with underscore)' ],
+
+                       # Icky variables that shouldn't link
+                       [
+                               '$myAwesomeVariable',
+                               '$myAwesomeVariable',
+                               'Testing $myAwesomeVariable (not starting with $wg)'
+                       ],
+                       [ '$()not!a&Var', '$()not!a&Var', 'Testing $()not!a&Var (obviously not a variable)' ],
+               ];
+       }
+}
diff --git a/tests/phpunit/unit/includes/installer/OracleInstallerTest.php b/tests/phpunit/unit/includes/installer/OracleInstallerTest.php
new file mode 100644 (file)
index 0000000..7dbb218
--- /dev/null
@@ -0,0 +1,49 @@
+<?php
+
+/**
+ * @group Database
+ * @group Installer
+ */
+class OracleInstallerTest extends \MediaWikiUnitTestCase {
+
+       /**
+        * @dataProvider provideOracleConnectStrings
+        * @covers OracleInstaller::checkConnectStringFormat
+        */
+       public function testCheckConnectStringFormat( $expected, $connectString, $msg = '' ) {
+               $validity = $expected ? 'should be valid' : 'should NOT be valid';
+               $msg = "'$connectString' ($msg) $validity.";
+               $this->assertEquals( $expected,
+                       OracleInstaller::checkConnectStringFormat( $connectString ),
+                       $msg
+               );
+       }
+
+       /**
+        * Provider to test OracleInstaller::checkConnectStringFormat()
+        */
+       function provideOracleConnectStrings() {
+               // expected result, connectString[, message]
+               return [
+                       [ true, 'simple_01', 'Simple TNS name' ],
+                       [ true, 'simple_01.world', 'TNS name with domain' ],
+                       [ true, 'simple_01.domain.net', 'TNS name with domain' ],
+                       [ true, 'host123', 'Host only' ],
+                       [ true, 'host123.domain.net', 'FQDN only' ],
+                       [ true, '//host123.domain.net', 'FQDN URL only' ],
+                       [ true, '123.223.213.132', 'Host IP only' ],
+                       [ true, 'host:1521', 'Host and port' ],
+                       [ true, 'host:1521/service', 'Host, port and service' ],
+                       [ true, 'host:1521/service:shared', 'Host, port, service and shared server type' ],
+                       [ true, 'host:1521/service:dedicated', 'Host, port, service and dedicated server type' ],
+                       [ true, 'host:1521/service:pooled', 'Host, port, service and pooled server type' ],
+                       [
+                               true,
+                               'host:1521/service:shared/instance1',
+                               'Host, port, service, server type and instance'
+                       ],
+                       [ true, 'host:1521//instance1', 'Host, port and instance' ],
+               ];
+       }
+
+}
diff --git a/tests/phpunit/unit/includes/interwiki/InterwikiLookupAdapterTest.php b/tests/phpunit/unit/includes/interwiki/InterwikiLookupAdapterTest.php
new file mode 100644 (file)
index 0000000..abbd2d7
--- /dev/null
@@ -0,0 +1,133 @@
+<?php
+
+use MediaWiki\Interwiki\InterwikiLookupAdapter;
+
+/**
+ * @covers MediaWiki\Interwiki\InterwikiLookupAdapter
+ *
+ * @group MediaWiki
+ * @group Interwiki
+ */
+class InterwikiLookupAdapterTest extends \MediaWikiUnitTestCase {
+
+       /**
+        * @var InterwikiLookupAdapter
+        */
+       private $interwikiLookup;
+
+       protected function setUp() {
+               parent::setUp();
+
+               $this->interwikiLookup = new InterwikiLookupAdapter(
+                       $this->getSiteLookup( $this->getSites() )
+               );
+       }
+
+       public function testIsValidInterwiki() {
+               $this->assertTrue(
+                       $this->interwikiLookup->isValidInterwiki( 'enwt' ),
+                       'enwt known prefix is valid'
+               );
+               $this->assertTrue(
+                       $this->interwikiLookup->isValidInterwiki( 'foo' ),
+                       'foo site known prefix is valid'
+               );
+               $this->assertFalse(
+                       $this->interwikiLookup->isValidInterwiki( 'xyz' ),
+                       'unknown prefix is not valid'
+               );
+       }
+
+       public function testFetch() {
+               $interwiki = $this->interwikiLookup->fetch( '' );
+               $this->assertNull( $interwiki );
+
+               $interwiki = $this->interwikiLookup->fetch( 'xyz' );
+               $this->assertFalse( $interwiki );
+
+               $interwiki = $this->interwikiLookup->fetch( 'foo' );
+               $this->assertInstanceOf( Interwiki::class, $interwiki );
+               $this->assertSame( 'foobar', $interwiki->getWikiID() );
+
+               $interwiki = $this->interwikiLookup->fetch( 'enwt' );
+               $this->assertInstanceOf( Interwiki::class, $interwiki );
+
+               $this->assertSame( 'https://en.wiktionary.org/wiki/$1', $interwiki->getURL(), 'getURL' );
+               $this->assertSame( 'https://en.wiktionary.org/w/api.php', $interwiki->getAPI(), 'getAPI' );
+               $this->assertSame( 'enwiktionary', $interwiki->getWikiID(), 'getWikiID' );
+               $this->assertTrue( $interwiki->isLocal(), 'isLocal' );
+       }
+
+       public function testGetAllPrefixes() {
+               $foo = [
+                       'iw_prefix' => 'foo',
+                       'iw_url' => '',
+                       'iw_api' => '',
+                       'iw_wikiid' => 'foobar',
+                       'iw_local' => false,
+                       'iw_trans' => false,
+               ];
+               $enwt = [
+                       'iw_prefix' => 'enwt',
+                       'iw_url' => 'https://en.wiktionary.org/wiki/$1',
+                       'iw_api' => 'https://en.wiktionary.org/w/api.php',
+                       'iw_wikiid' => 'enwiktionary',
+                       'iw_local' => true,
+                       'iw_trans' => false,
+               ];
+
+               $this->assertEquals(
+                       [ $foo, $enwt ],
+                       $this->interwikiLookup->getAllPrefixes(),
+                       'getAllPrefixes()'
+               );
+
+               $this->assertEquals(
+                       [ $foo ],
+                       $this->interwikiLookup->getAllPrefixes( false ),
+                       'get external prefixes'
+               );
+
+               $this->assertEquals(
+                       [ $enwt ],
+                       $this->interwikiLookup->getAllPrefixes( true ),
+                       'get local prefixes'
+               );
+       }
+
+       private function getSiteLookup( SiteList $sites ) {
+               $siteLookup = $this->getMockBuilder( SiteLookup::class )
+                       ->disableOriginalConstructor()
+                       ->getMock();
+
+               $siteLookup->expects( $this->any() )
+                       ->method( 'getSites' )
+                       ->will( $this->returnValue( $sites ) );
+
+               return $siteLookup;
+       }
+
+       private function getSites() {
+               $sites = [];
+
+               $site = new Site();
+               $site->setGlobalId( 'foobar' );
+               $site->addInterwikiId( 'foo' );
+               $site->setSource( 'external' );
+               $sites[] = $site;
+
+               $site = new MediaWikiSite();
+               $site->setGlobalId( 'enwiktionary' );
+               $site->setGroup( 'wiktionary' );
+               $site->setLanguageCode( 'en' );
+               $site->addNavigationId( 'enwiktionary' );
+               $site->addInterwikiId( 'enwt' );
+               $site->setSource( 'local' );
+               $site->setPath( MediaWikiSite::PATH_PAGE, "https://en.wiktionary.org/wiki/$1" );
+               $site->setPath( MediaWikiSite::PATH_FILE, "https://en.wiktionary.org/w/$1" );
+               $sites[] = $site;
+
+               return new SiteList( $sites );
+       }
+
+}
diff --git a/tests/phpunit/unit/includes/jobqueue/JobQueueMemoryTest.php b/tests/phpunit/unit/includes/jobqueue/JobQueueMemoryTest.php
new file mode 100644 (file)
index 0000000..232b46a
--- /dev/null
@@ -0,0 +1,63 @@
+<?php
+
+/**
+ * @covers JobQueueMemory
+ *
+ * @group JobQueue
+ *
+ * @license GPL-2.0-or-later
+ * @author Thiemo Kreuz
+ */
+class JobQueueMemoryTest extends PHPUnit\Framework\TestCase {
+
+       use MediaWikiCoversValidator;
+
+       /**
+        * @return JobQueueMemory
+        */
+       private function newJobQueue() {
+               return JobQueue::factory( [
+                       'class' => JobQueueMemory::class,
+                       'domain' => WikiMap::getCurrentWikiDbDomain()->getId(),
+                       'type' => 'null',
+               ] );
+       }
+
+       private function newJobSpecification() {
+               return new JobSpecification(
+                       'null',
+                       [ 'customParameter' => null ],
+                       [],
+                       Title::newFromText( 'Custom title' )
+               );
+       }
+
+       public function testGetAllQueuedJobs() {
+               $queue = $this->newJobQueue();
+               $this->assertCount( 0, $queue->getAllQueuedJobs() );
+
+               $queue->push( $this->newJobSpecification() );
+               $this->assertCount( 1, $queue->getAllQueuedJobs() );
+       }
+
+       public function testGetAllAcquiredJobs() {
+               $queue = $this->newJobQueue();
+               $this->assertCount( 0, $queue->getAllAcquiredJobs() );
+
+               $queue->push( $this->newJobSpecification() );
+               $this->assertCount( 0, $queue->getAllAcquiredJobs() );
+
+               $queue->pop();
+               $this->assertCount( 1, $queue->getAllAcquiredJobs() );
+       }
+
+       public function testJobFromSpecInternal() {
+               $queue = $this->newJobQueue();
+               $job = $queue->jobFromSpecInternal( $this->newJobSpecification() );
+               $this->assertInstanceOf( Job::class, $job );
+               $this->assertSame( 'null', $job->getType() );
+               $this->assertArrayHasKey( 'customParameter', $job->getParams() );
+               $this->assertSame( 'Custom title', $job->getTitle()->getText() );
+       }
+
+}
diff --git a/tests/phpunit/unit/includes/json/FormatJsonTest.php b/tests/phpunit/unit/includes/json/FormatJsonTest.php
new file mode 100644 (file)
index 0000000..f07eea7
--- /dev/null
@@ -0,0 +1,436 @@
+<?php
+
+/**
+ * @covers FormatJson
+ */
+class FormatJsonTest extends \MediaWikiUnitTestCase {
+
+       public static function provideEncoderPrettyPrinting() {
+               return [
+                       // Four spaces
+                       [ true, '    ' ],
+                       [ '    ', '    ' ],
+                       // Two spaces
+                       [ '  ', '  ' ],
+                       // One tab
+                       [ "\t", "\t" ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideEncoderPrettyPrinting
+        */
+       public function testEncoderPrettyPrinting( $pretty, $expectedIndent ) {
+               $obj = [
+                       'emptyObject' => new stdClass,
+                       'emptyArray' => [],
+                       'string' => 'foobar\\',
+                       'filledArray' => [
+                               [
+                                       123,
+                                       456,
+                               ],
+                               // Nested json works without problems
+                               '"7":["8",{"9":"10"}]',
+                               // Whitespace clean up doesn't touch strings that look alike
+                               "{\n\t\"emptyObject\": {\n\t},\n\t\"emptyArray\": [ ]\n}",
+                       ],
+               ];
+
+               // No trailing whitespace, no trailing linefeed
+               $json = '{
+       "emptyObject": {},
+       "emptyArray": [],
+       "string": "foobar\\\\",
+       "filledArray": [
+               [
+                       123,
+                       456
+               ],
+               "\"7\":[\"8\",{\"9\":\"10\"}]",
+               "{\n\t\"emptyObject\": {\n\t},\n\t\"emptyArray\": [ ]\n}"
+       ]
+}';
+
+               $json = str_replace( "\r", '', $json ); // Windows compat
+               $json = str_replace( "\t", $expectedIndent, $json );
+               $this->assertSame( $json, FormatJson::encode( $obj, $pretty ) );
+       }
+
+       public static function provideEncodeDefault() {
+               return self::getEncodeTestCases( [] );
+       }
+
+       /**
+        * @dataProvider provideEncodeDefault
+        */
+       public function testEncodeDefault( $from, $to ) {
+               $this->assertSame( $to, FormatJson::encode( $from ) );
+       }
+
+       public static function provideEncodeUtf8() {
+               return self::getEncodeTestCases( [ 'unicode' ] );
+       }
+
+       /**
+        * @dataProvider provideEncodeUtf8
+        */
+       public function testEncodeUtf8( $from, $to ) {
+               $this->assertSame( $to, FormatJson::encode( $from, false, FormatJson::UTF8_OK ) );
+       }
+
+       public static function provideEncodeXmlMeta() {
+               return self::getEncodeTestCases( [ 'xmlmeta' ] );
+       }
+
+       /**
+        * @dataProvider provideEncodeXmlMeta
+        */
+       public function testEncodeXmlMeta( $from, $to ) {
+               $this->assertSame( $to, FormatJson::encode( $from, false, FormatJson::XMLMETA_OK ) );
+       }
+
+       public static function provideEncodeAllOk() {
+               return self::getEncodeTestCases( [ 'unicode', 'xmlmeta' ] );
+       }
+
+       /**
+        * @dataProvider provideEncodeAllOk
+        */
+       public function testEncodeAllOk( $from, $to ) {
+               $this->assertSame( $to, FormatJson::encode( $from, false, FormatJson::ALL_OK ) );
+       }
+
+       public function testEncodePhpBug46944() {
+               $this->assertNotEquals(
+                       '\ud840\udc00',
+                       strtolower( FormatJson::encode( "\xf0\xa0\x80\x80" ) ),
+                       'Test encoding an broken json_encode character (U+20000)'
+               );
+       }
+
+       public function testEncodeFail() {
+               // Set up a recursive object that can't be encoded.
+               $a = new stdClass;
+               $b = new stdClass;
+               $a->b = $b;
+               $b->a = $a;
+               $this->assertFalse( FormatJson::encode( $a ) );
+       }
+
+       public function testDecodeReturnType() {
+               $this->assertInternalType(
+                       'object',
+                       FormatJson::decode( '{"Name": "Cheeso", "Rank": 7}' ),
+                       'Default to object'
+               );
+
+               $this->assertInternalType(
+                       'array',
+                       FormatJson::decode( '{"Name": "Cheeso", "Rank": 7}', true ),
+                       'Optional array'
+               );
+       }
+
+       public static function provideParse() {
+               return [
+                       [ null ],
+                       [ true ],
+                       [ false ],
+                       [ 0 ],
+                       [ 1 ],
+                       [ 1.2 ],
+                       [ '' ],
+                       [ 'str' ],
+                       [ [ 0, 1, 2 ] ],
+                       [ [ 'a' => 'b' ] ],
+                       [ [ 'a' => 'b' ] ],
+                       [ [ 'a' => 'b', 'x' => [ 'c' => 'd' ] ] ],
+               ];
+       }
+
+       /**
+        * Recursively convert arrays into stdClass
+        * @param array|string|bool|int|float|null $value
+        * @return stdClass|string|bool|int|float|null
+        */
+       public static function toObject( $value ) {
+               return !is_array( $value ) ? $value : (object)array_map( __METHOD__, $value );
+       }
+
+       /**
+        * @dataProvider provideParse
+        * @param mixed $value
+        */
+       public function testParse( $value ) {
+               $expected = self::toObject( $value );
+               $json = FormatJson::encode( $expected, false, FormatJson::ALL_OK );
+               $this->assertJson( $json );
+
+               $st = FormatJson::parse( $json );
+               $this->assertInstanceOf( Status::class, $st );
+               $this->assertTrue( $st->isGood() );
+               $this->assertEquals( $expected, $st->getValue() );
+
+               $st = FormatJson::parse( $json, FormatJson::FORCE_ASSOC );
+               $this->assertInstanceOf( Status::class, $st );
+               $this->assertTrue( $st->isGood() );
+               $this->assertEquals( $value, $st->getValue() );
+       }
+
+       /**
+        * Test data for testParseTryFixing.
+        *
+        * Some PHP interpreters use json-c rather than the JSON.org canonical
+        * parser to avoid being encumbered by the "shall be used for Good, not
+        * Evil" clause of the JSON.org parser's license. By default, json-c
+        * parses in a non-strict mode which allows trailing commas for array and
+        * object delarations among other things, so our JSON_ERROR_SYNTAX rescue
+        * block is not always triggered. It however isn't lenient in exactly the
+        * same ways as our TRY_FIXING mode, so the assertions in this test are
+        * a bit more complicated than they ideally would be:
+        *
+        * Optional third argument: true if json-c parses the value without
+        * intervention, false otherwise. Defaults to true.
+        *
+        * Optional fourth argument: expected cannonical JSON serialization of
+        * json-c parsed result. Defaults to the second argument's value.
+        */
+       public static function provideParseTryFixing() {
+               return [
+                       [ "[,]", '[]', false ],
+                       [ "[ , ]", '[]', false ],
+                       [ "[ , }", false ],
+                       [ '[1],', false, true, '[1]' ],
+                       [ "[1,]", '[1]' ],
+                       [ "[1\n,]", '[1]' ],
+                       [ "[1,\n]", '[1]' ],
+                       [ "[1,]\n", '[1]' ],
+                       [ "[1\n,\n]\n", '[1]' ],
+                       [ '["a,",]', '["a,"]' ],
+                       [ "[[1,]\n,[2,\n],[3\n,]]", '[[1],[2],[3]]' ],
+                       // I wish we could parse this, but would need quote parsing
+                       [ '[[1,],[2,],[3,]]', false, true, '[[1],[2],[3]]' ],
+                       [ '[1,,]', false, false, '[1]' ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideParseTryFixing
+        * @param string $value
+        * @param string|bool $expected Expected result with strict parser
+        * @param bool $jsoncParses Will json-c parse this value without TRY_FIXING?
+        * @param string|bool $expectedJsonc Expected result with lenient parser
+        * if different from the strict expectation
+        */
+       public function testParseTryFixing(
+               $value, $expected,
+               $jsoncParses = true, $expectedJsonc = null
+       ) {
+               // PHP5 results are always expected to have isGood() === false
+               $expectedGoodStatus = false;
+
+               // Check to see if json parser allows trailing commas
+               if ( json_decode( '[1,]' ) !== null ) {
+                       // Use json-c specific expected result if provided
+                       $expected = ( $expectedJsonc === null ) ? $expected : $expectedJsonc;
+                       // If json-c parses the value natively, expect isGood() === true
+                       $expectedGoodStatus = $jsoncParses;
+               }
+
+               $st = FormatJson::parse( $value, FormatJson::TRY_FIXING );
+               $this->assertInstanceOf( Status::class, $st );
+               if ( $expected === false ) {
+                       $this->assertFalse( $st->isOK(), 'Expected isOK() == false' );
+               } else {
+                       $this->assertSame( $expectedGoodStatus, $st->isGood(),
+                               'Expected isGood() == ' . ( $expectedGoodStatus ? 'true' : 'false' )
+                       );
+                       $this->assertTrue( $st->isOK(), 'Expected isOK == true' );
+                       $val = FormatJson::encode( $st->getValue(), false, FormatJson::ALL_OK );
+                       $this->assertEquals( $expected, $val );
+               }
+       }
+
+       public static function provideParseErrors() {
+               return [
+                       [ 'aaa' ],
+                       [ '{"j": 1 ] }' ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideParseErrors
+        * @param mixed $value
+        */
+       public function testParseErrors( $value ) {
+               $st = FormatJson::parse( $value );
+               $this->assertInstanceOf( Status::class, $st );
+               $this->assertFalse( $st->isOK() );
+       }
+
+       public function provideStripComments() {
+               return [
+                       [ '{"a":"b"}', '{"a":"b"}' ],
+                       [ "{\"a\":\"b\"}\n", "{\"a\":\"b\"}\n" ],
+                       [ '/*c*/{"c":"b"}', '{"c":"b"}' ],
+                       [ '{"a":"c"}/*c*/', '{"a":"c"}' ],
+                       [ '/*c//d*/{"c":"b"}', '{"c":"b"}' ],
+                       [ '{/*c*/"c":"b"}', '{"c":"b"}' ],
+                       [ "/*\nc\r\n*/{\"c\":\"b\"}", '{"c":"b"}' ],
+                       [ "//c\n{\"c\":\"b\"}", '{"c":"b"}' ],
+                       [ "//c\r\n{\"c\":\"b\"}", '{"c":"b"}' ],
+                       [ '{"a":"c"}//c', '{"a":"c"}' ],
+                       [ "{\"a-c\"://c\n\"b\"}", '{"a-c":"b"}' ],
+                       [ '{"/*a":"b"}', '{"/*a":"b"}' ],
+                       [ '{"a":"//b"}', '{"a":"//b"}' ],
+                       [ '{"a":"b/*c*/"}', '{"a":"b/*c*/"}' ],
+                       [ "{\"\\\"/*a\":\"b\"}", "{\"\\\"/*a\":\"b\"}" ],
+                       [ '', '' ],
+                       [ '/*c', '' ],
+                       [ '//c', '' ],
+                       [ '"http://example.com"', '"http://example.com"' ],
+                       [ "\0", "\0" ],
+                       [ '"Blåbærsyltetøy"', '"Blåbærsyltetøy"' ],
+               ];
+       }
+
+       /**
+        * @covers FormatJson::stripComments
+        * @dataProvider provideStripComments
+        * @param string $json
+        * @param string $expect
+        */
+       public function testStripComments( $json, $expect ) {
+               $this->assertSame( $expect, FormatJson::stripComments( $json ) );
+       }
+
+       public function provideParseStripComments() {
+               return [
+                       [ '/* blah */true', true ],
+                       [ "// blah \ntrue", true ],
+                       [ '[ "a" , /* blah */ "b" ]', [ 'a', 'b' ] ],
+               ];
+       }
+
+       /**
+        * @covers FormatJson::parse
+        * @covers FormatJson::stripComments
+        * @dataProvider provideParseStripComments
+        * @param string $json
+        * @param mixed $expect
+        */
+       public function testParseStripComments( $json, $expect ) {
+               $st = FormatJson::parse( $json, FormatJson::STRIP_COMMENTS );
+               $this->assertInstanceOf( Status::class, $st );
+               $this->assertTrue( $st->isGood() );
+               $this->assertEquals( $expect, $st->getValue() );
+       }
+
+       /**
+        * Generate a set of test cases for a particular combination of encoder options.
+        *
+        * @param array $unescapedGroups List of character groups to leave unescaped
+        * @return array Arrays of unencoded strings and corresponding encoded strings
+        */
+       private static function getEncodeTestCases( array $unescapedGroups ) {
+               $groups = [
+                       'always' => [
+                               // Forward slash (always unescaped)
+                               '/' => '/',
+
+                               // Control characters
+                               "\0" => '\u0000',
+                               "\x08" => '\b',
+                               "\t" => '\t',
+                               "\n" => '\n',
+                               "\r" => '\r',
+                               "\f" => '\f',
+                               "\x1f" => '\u001f', // representative example
+
+                               // Double quotes
+                               '"' => '\"',
+
+                               // Backslashes
+                               '\\' => '\\\\',
+                               '\\\\' => '\\\\\\\\',
+                               '\\u00e9' => '\\\u00e9', // security check for Unicode unescaping
+
+                               // Line terminators
+                               "\xe2\x80\xa8" => '\u2028',
+                               "\xe2\x80\xa9" => '\u2029',
+                       ],
+                       'unicode' => [
+                               "\xc3\xa9" => '\u00e9',
+                               "\xf0\x9d\x92\x9e" => '\ud835\udc9e', // U+1D49E, outside the BMP
+                       ],
+                       'xmlmeta' => [
+                               '<' => '\u003C', // JSON_HEX_TAG uses uppercase hex digits
+                               '>' => '\u003E',
+                               '&' => '\u0026',
+                       ],
+               ];
+
+               $cases = [];
+               foreach ( $groups as $name => $rules ) {
+                       $leaveUnescaped = in_array( $name, $unescapedGroups );
+                       foreach ( $rules as $from => $to ) {
+                               $cases[] = [ $from, '"' . ( $leaveUnescaped ? $from : $to ) . '"' ];
+                       }
+               }
+
+               return $cases;
+       }
+
+       public function provideEmptyJsonKeyStrings() {
+               return [
+                       [
+                               '{"":"foo"}',
+                               '{"":"foo"}',
+                               ''
+                       ],
+                       [
+                               '{"_empty_":"foo"}',
+                               '{"_empty_":"foo"}',
+                               '_empty_' ],
+                       [
+                               '{"\u005F\u0065\u006D\u0070\u0074\u0079\u005F":"foo"}',
+                               '{"_empty_":"foo"}',
+                               '_empty_'
+                       ],
+                       [
+                               '{"_empty_":"bar","":"foo"}',
+                               '{"_empty_":"bar","":"foo"}',
+                               ''
+                       ],
+                       [
+                               '{"":"bar","_empty_":"foo"}',
+                               '{"":"bar","_empty_":"foo"}',
+                               '_empty_'
+                       ]
+               ];
+       }
+
+       /**
+        * @covers FormatJson::encode
+        * @covers FormatJson::decode
+        * @dataProvider provideEmptyJsonKeyStrings
+        * @param string $json
+        *
+        * Decoding behavior with empty keys can be surprising.
+        * See https://phabricator.wikimedia.org/T206411
+        */
+       public function testEmptyJsonKeyArray( $json, $expect, $php71Name ) {
+               // Decoding to array is consistent across supported PHP versions
+               $this->assertSame( $expect, FormatJson::encode(
+                       FormatJson::decode( $json, true ) ) );
+
+               // Decoding to object differs between supported PHP versions
+               $obj = FormatJson::decode( $json );
+               if ( version_compare( PHP_VERSION, '7.1', '<' ) ) {
+                       $this->assertEquals( 'foo', $obj->_empty_ );
+               } else {
+                       $this->assertEquals( 'foo', $obj->{$php71Name} );
+               }
+       }
+}
diff --git a/tests/phpunit/unit/includes/libs/ArrayUtilsTest.php b/tests/phpunit/unit/includes/libs/ArrayUtilsTest.php
new file mode 100644 (file)
index 0000000..12b6320
--- /dev/null
@@ -0,0 +1,308 @@
+<?php
+/**
+ * Test class for ArrayUtils class
+ *
+ * @group Database
+ */
+class ArrayUtilsTest extends PHPUnit\Framework\TestCase {
+
+       use MediaWikiCoversValidator;
+
+       /**
+        * @covers ArrayUtils::findLowerBound
+        * @dataProvider provideFindLowerBound
+        */
+       function testFindLowerBound(
+               $valueCallback, $valueCount, $comparisonCallback, $target, $expected
+       ) {
+               $this->assertSame(
+                       ArrayUtils::findLowerBound(
+                               $valueCallback, $valueCount, $comparisonCallback, $target
+                       ), $expected
+               );
+       }
+
+       function provideFindLowerBound() {
+               $indexValueCallback = function ( $size ) {
+                       return function ( $val ) use ( $size ) {
+                               $this->assertTrue( $val >= 0 );
+                               $this->assertTrue( $val < $size );
+                               return $val;
+                       };
+               };
+               $comparisonCallback = function ( $a, $b ) {
+                       return $a - $b;
+               };
+
+               return [
+                       [
+                               $indexValueCallback( 0 ),
+                               0,
+                               $comparisonCallback,
+                               1,
+                               false,
+                       ],
+                       [
+                               $indexValueCallback( 1 ),
+                               1,
+                               $comparisonCallback,
+                               -1,
+                               false,
+                       ],
+                       [
+                               $indexValueCallback( 1 ),
+                               1,
+                               $comparisonCallback,
+                               0,
+                               0,
+                       ],
+                       [
+                               $indexValueCallback( 1 ),
+                               1,
+                               $comparisonCallback,
+                               1,
+                               0,
+                       ],
+                       [
+                               $indexValueCallback( 2 ),
+                               2,
+                               $comparisonCallback,
+                               -1,
+                               false,
+                       ],
+                       [
+                               $indexValueCallback( 2 ),
+                               2,
+                               $comparisonCallback,
+                               0,
+                               0,
+                       ],
+                       [
+                               $indexValueCallback( 2 ),
+                               2,
+                               $comparisonCallback,
+                               0.5,
+                               0,
+                       ],
+                       [
+                               $indexValueCallback( 2 ),
+                               2,
+                               $comparisonCallback,
+                               1,
+                               1,
+                       ],
+                       [
+                               $indexValueCallback( 2 ),
+                               2,
+                               $comparisonCallback,
+                               1.5,
+                               1,
+                       ],
+                       [
+                               $indexValueCallback( 3 ),
+                               3,
+                               $comparisonCallback,
+                               1,
+                               1,
+                       ],
+                       [
+                               $indexValueCallback( 3 ),
+                               3,
+                               $comparisonCallback,
+                               1.5,
+                               1,
+                       ],
+                       [
+                               $indexValueCallback( 3 ),
+                               3,
+                               $comparisonCallback,
+                               2,
+                               2,
+                       ],
+                       [
+                               $indexValueCallback( 3 ),
+                               3,
+                               $comparisonCallback,
+                               3,
+                               2,
+                       ],
+               ];
+       }
+
+       /**
+        * @covers ArrayUtils::arrayDiffAssocRecursive
+        * @dataProvider provideArrayDiffAssocRecursive
+        */
+       function testArrayDiffAssocRecursive( $expected, ...$args ) {
+               $this->assertEquals( call_user_func_array(
+                       'ArrayUtils::arrayDiffAssocRecursive', $args
+               ), $expected );
+       }
+
+       function provideArrayDiffAssocRecursive() {
+               return [
+                       [
+                               [],
+                               [],
+                               [],
+                       ],
+                       [
+                               [],
+                               [],
+                               [],
+                               [],
+                       ],
+                       [
+                               [ 1 ],
+                               [ 1 ],
+                               [],
+                       ],
+                       [
+                               [ 1 ],
+                               [ 1 ],
+                               [],
+                               [],
+                       ],
+                       [
+                               [],
+                               [],
+                               [ 1 ],
+                       ],
+                       [
+                               [],
+                               [],
+                               [ 1 ],
+                               [ 2 ],
+                       ],
+                       [
+                               [ '' => 1 ],
+                               [ '' => 1 ],
+                               [],
+                       ],
+                       [
+                               [],
+                               [],
+                               [ '' => 1 ],
+                       ],
+                       [
+                               [ 1 ],
+                               [ 1 ],
+                               [ 2 ],
+                       ],
+                       [
+                               [],
+                               [ 1 ],
+                               [ 2 ],
+                               [ 1 ],
+                       ],
+                       [
+                               [],
+                               [ 1 ],
+                               [ 1, 2 ],
+                       ],
+                       [
+                               [ 1 => 1 ],
+                               [ 1 => 1 ],
+                               [ 1 ],
+                       ],
+                       [
+                               [],
+                               [ 1 => 1 ],
+                               [ 1 ],
+                               [ 1 => 1 ],
+                       ],
+                       [
+                               [],
+                               [ 1 => 1 ],
+                               [ 1, 1, 1 ],
+                       ],
+                       [
+                               [],
+                               [ [] ],
+                               [],
+                       ],
+                       [
+                               [],
+                               [ [ [] ] ],
+                               [],
+                       ],
+                       [
+                               [ 1, [ 1 ] ],
+                               [ 1, [ 1 ] ],
+                               [],
+                       ],
+                       [
+                               [ 1 ],
+                               [ 1, [ 1 ] ],
+                               [ 2, [ 1 ] ],
+                       ],
+                       [
+                               [],
+                               [ 1, [ 1 ] ],
+                               [ 2, [ 1 ] ],
+                               [ 1, [ 2 ] ],
+                       ],
+                       [
+                               [ 1 ],
+                               [ 1, [] ],
+                               [ 2 ],
+                       ],
+                       [
+                               [],
+                               [ 1, [] ],
+                               [ 2 ],
+                               [ 1 ],
+                       ],
+                       [
+                               [ 1, [ 1 => 2 ] ],
+                               [ 1, [ 1, 2 ] ],
+                               [ 2, [ 1 ] ],
+                       ],
+                       [
+                               [ 1 ],
+                               [ 1, [ 1, 2 ] ],
+                               [ 2, [ 1 ] ],
+                               [ 2, [ 1 => 2 ] ],
+                       ],
+                       [
+                               [ 1 => [ 1, 2 ] ],
+                               [ 1, [ 1, 2 ] ],
+                               [ 1, [ 2 ] ],
+                       ],
+                       [
+                               [ 1 => [ [ 2, 3 ], 2 ] ],
+                               [ 1, [ [ 2, 3 ], 2 ] ],
+                               [ 1, [ 2 ] ],
+                       ],
+                       [
+                               [ 1 => [ [ 2 ], 2 ] ],
+                               [ 1, [ [ 2, 3 ], 2 ] ],
+                               [ 1, [ [ 1 => 3 ] ] ],
+                       ],
+                       [
+                               [ 1 => [ 1 => 2 ] ],
+                               [ 1, [ [ 2, 3 ], 2 ] ],
+                               [ 1, [ [ 1 => 3, 0 => 2 ] ] ],
+                       ],
+                       [
+                               [ 1 => [ 1 => 2 ] ],
+                               [ 1, [ [ 2, 3 ], 2 ] ],
+                               [ 1, [ [ 1 => 3 ] ] ],
+                               [ 1 => [ [ 2 ] ] ],
+                       ],
+                       [
+                               [],
+                               [ 1, [ [ 2, 3 ], 2 ] ],
+                               [ 1 => [ 1 => 2, 0 => [ 1 => 3, 0 => 2 ] ], 0 => 1 ],
+                       ],
+                       [
+                               [],
+                               [ 1, [ [ 2, 3 ], 2 ] ],
+                               [ 1 => [ 1 => 2 ] ],
+                               [ 1 => [ [ 1 => 3 ] ] ],
+                               [ 1 => [ [ 2 ] ] ],
+                               [ 1 ],
+                       ],
+               ];
+       }
+}
diff --git a/tests/phpunit/unit/includes/libs/CookieTest.php b/tests/phpunit/unit/includes/libs/CookieTest.php
new file mode 100644 (file)
index 0000000..e383be9
--- /dev/null
@@ -0,0 +1,52 @@
+<?php
+
+/**
+ * @covers Cookie
+ */
+class CookieTest extends \PHPUnit\Framework\TestCase {
+
+       /**
+        * @dataProvider cookieDomains
+        * @covers Cookie::validateCookieDomain
+        */
+       public function testValidateCookieDomain( $expected, $domain, $origin = null ) {
+               if ( $origin ) {
+                       $ok = Cookie::validateCookieDomain( $domain, $origin );
+                       $msg = "$domain against origin $origin";
+               } else {
+                       $ok = Cookie::validateCookieDomain( $domain );
+                       $msg = "$domain";
+               }
+               $this->assertEquals( $expected, $ok, $msg );
+       }
+
+       public static function cookieDomains() {
+               return [
+                       [ false, "org" ],
+                       [ false, ".org" ],
+                       [ true, "wikipedia.org" ],
+                       [ true, ".wikipedia.org" ],
+                       [ false, "co.uk" ],
+                       [ false, ".co.uk" ],
+                       [ false, "gov.uk" ],
+                       [ false, ".gov.uk" ],
+                       [ true, "supermarket.uk" ],
+                       [ false, "uk" ],
+                       [ false, ".uk" ],
+                       [ false, "127.0.0." ],
+                       [ false, "127." ],
+                       [ false, "127.0.0.1." ],
+                       [ true, "127.0.0.1" ],
+                       [ false, "333.0.0.1" ],
+                       [ true, "example.com" ],
+                       [ false, "example.com." ],
+                       [ true, ".example.com" ],
+
+                       [ true, ".example.com", "www.example.com" ],
+                       [ false, "example.com", "www.example.com" ],
+                       [ true, "127.0.0.1", "127.0.0.1" ],
+                       [ false, "127.0.0.1", "localhost" ],
+               ];
+       }
+
+}
diff --git a/tests/phpunit/unit/includes/libs/DeferredStringifierTest.php b/tests/phpunit/unit/includes/libs/DeferredStringifierTest.php
new file mode 100644 (file)
index 0000000..c9cdf58
--- /dev/null
@@ -0,0 +1,54 @@
+<?php
+
+/**
+ * @covers DeferredStringifier
+ */
+class DeferredStringifierTest extends PHPUnit\Framework\TestCase {
+
+       use MediaWikiCoversValidator;
+
+       /**
+        * @dataProvider provideToString
+        */
+       public function testToString( $params, $expected ) {
+               $class = new ReflectionClass( DeferredStringifier::class );
+               $ds = $class->newInstanceArgs( $params );
+               $this->assertEquals( $expected, (string)$ds );
+       }
+
+       public static function provideToString() {
+               return [
+                       // No args
+                       [
+                               [
+                                       function () {
+                                               return 'foo';
+                                       }
+                               ],
+                               'foo'
+                       ],
+                       // Has args
+                       [
+                               [
+                                       function ( $i ) {
+                                               return $i;
+                                       },
+                                       'bar'
+                               ],
+                               'bar'
+                       ],
+               ];
+       }
+
+       /**
+        * Verify that the callback is not called if
+        * it is never converted to a string
+        */
+       public function testCallbackNotCalled() {
+               $ds = new DeferredStringifier( function () {
+                       throw new Exception( 'This should not be reached!' );
+               } );
+               // No exception was thrown
+               $this->assertTrue( true );
+       }
+}
diff --git a/tests/phpunit/unit/includes/libs/DnsSrvDiscovererTest.php b/tests/phpunit/unit/includes/libs/DnsSrvDiscovererTest.php
new file mode 100644 (file)
index 0000000..1b3397c
--- /dev/null
@@ -0,0 +1,144 @@
+<?php
+
+/**
+ * @covers DnsSrvDiscoverer
+ */
+class DnsSrvDiscovererTest extends PHPUnit\Framework\TestCase {
+
+       use MediaWikiCoversValidator;
+
+       /**
+        * @dataProvider provideRecords
+        */
+       public function testPickServer( $params, $expected ) {
+               $discoverer = new DnsSrvDiscoverer( 'etcd-tcp.example.net' );
+               $record = $discoverer->pickServer( $params );
+
+               $this->assertEquals( $expected, $record );
+       }
+
+       public static function provideRecords() {
+               return [
+                       [
+                               [ // record list
+                                       [
+                                               'target' => 'conf03.example.net',
+                                               'port' => 'SRV',
+                                               'pri' => 0,
+                                               'weight' => 1,
+                                       ],
+                                       [
+                                               'target' => 'conf02.example.net',
+                                               'port' => 'SRV',
+                                               'pri' => 1,
+                                               'weight' => 1,
+                                       ],
+                                       [
+                                               'target' => 'conf01.example.net',
+                                               'port' => 'SRV',
+                                               'pri' => 2,
+                                               'weight' => 1,
+                                       ],
+                               ], // selected record
+                               [
+                                       'target' => 'conf03.example.net',
+                                       'port' => 'SRV',
+                                       'pri' => 0,
+                                       'weight' => 1,
+                               ]
+                       ],
+                       [
+                               [ // record list
+                                       [
+                                               'target' => 'conf03or2.example.net',
+                                               'port' => 'SRV',
+                                               'pri' => 0,
+                                               'weight' => 1,
+                                       ],
+                                       [
+                                               'target' => 'conf03or2.example.net',
+                                               'port' => 'SRV',
+                                               'pri' => 0,
+                                               'weight' => 1,
+                                       ],
+                                       [
+                                               'target' => 'conf01.example.net',
+                                               'port' => 'SRV',
+                                               'pri' => 2,
+                                               'weight' => 1,
+                                       ],
+                                       [
+                                               'target' => 'conf04.example.net',
+                                               'port' => 'SRV',
+                                               'pri' => 2,
+                                               'weight' => 1,
+                                       ],
+                                       [
+                                               'target' => 'conf05.example.net',
+                                               'port' => 'SRV',
+                                               'pri' => 3,
+                                               'weight' => 1,
+                                       ],
+                               ], // selected record
+                               [
+                                       'target' => 'conf03or2.example.net',
+                                       'port' => 'SRV',
+                                       'pri' => 0,
+                                       'weight' => 1,
+                               ]
+                       ],
+               ];
+       }
+
+       public function testRemoveServer() {
+               $dsd = new DnsSrvDiscoverer( 'localhost' );
+
+               $servers = [
+                       [
+                               'target' => 'conf01.example.net',
+                               'port' => 35,
+                               'pri' => 2,
+                               'weight' => 1,
+                       ],
+                       [
+                               'target' => 'conf04.example.net',
+                               'port' => 74,
+                               'pri' => 2,
+                               'weight' => 1,
+                       ],
+                       [
+                               'target' => 'conf05.example.net',
+                               'port' => 77,
+                               'pri' => 3,
+                               'weight' => 1,
+                       ],
+               ];
+               $server = $servers[1];
+
+               $expected = [
+                       [
+                               'target' => 'conf01.example.net',
+                               'port' => 35,
+                               'pri' => 2,
+                               'weight' => 1,
+                       ],
+                       [
+                               'target' => 'conf05.example.net',
+                               'port' => 77,
+                               'pri' => 3,
+                               'weight' => 1,
+                       ],
+               ];
+
+               $this->assertEquals(
+                       $expected,
+                       $dsd->removeServer( $server, $servers ),
+                       "Correct server removed"
+               );
+               $this->assertEquals(
+                       $expected,
+                       $dsd->removeServer( $server, $servers ),
+                       "Nothing to remove"
+               );
+       }
+}
diff --git a/tests/phpunit/unit/includes/libs/EasyDeflateTest.php b/tests/phpunit/unit/includes/libs/EasyDeflateTest.php
new file mode 100644 (file)
index 0000000..da39d48
--- /dev/null
@@ -0,0 +1,64 @@
+<?php
+/**
+ * Copyright (C) 2018 Kunal Mehta <legoktm@member.fsf.org>
+ *
+ * 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; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * 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.
+ *
+ */
+
+/**
+ * @covers EasyDeflate
+ */
+class EasyDeflateTest extends PHPUnit\Framework\TestCase {
+
+       public function provideIsDeflated() {
+               return [
+                       [ 'rawdeflate,S8vPT0osAgA=', true ],
+                       [ 'abcdefghijklmnopqrstuvwxyz', false ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideIsDeflated
+        */
+       public function testIsDeflated( $data, $expected ) {
+               $actual = EasyDeflate::isDeflated( $data );
+               $this->assertSame( $expected, $actual );
+       }
+
+       public function provideInflate() {
+               return [
+                       [ 'rawdeflate,S8vPT0osAgA=', true, 'foobar' ],
+                       // Fails base64_decode
+                       [ 'rawdeflate,🌻', false, 'easydeflate-invaliddeflate' ],
+                       // Fails gzinflate
+                       [ 'rawdeflate,S8vPT0dfdAgB=', false, 'easydeflate-invaliddeflate' ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideInflate
+        */
+       public function testInflate( $data, $ok, $value ) {
+               $actual = EasyDeflate::inflate( $data );
+               if ( $ok ) {
+                       $this->assertTrue( $actual->isOK() );
+                       $this->assertSame( $value, $actual->getValue() );
+               } else {
+                       $this->assertFalse( $actual->isOK() );
+                       $this->assertTrue( $actual->hasMessage( $value ) );
+               }
+       }
+}
diff --git a/tests/phpunit/unit/includes/libs/GenericArrayObjectTest.php b/tests/phpunit/unit/includes/libs/GenericArrayObjectTest.php
new file mode 100644 (file)
index 0000000..3be2b06
--- /dev/null
@@ -0,0 +1,279 @@
+<?php
+
+/**
+ * Tests for the GenericArrayObject and deriving classes.
+ *
+ * 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; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * 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.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @since 1.20
+ *
+ * @ingroup Test
+ * @group GenericArrayObject
+ *
+ * @author Jeroen De Dauw < jeroendedauw@gmail.com >
+ */
+abstract class GenericArrayObjectTest extends PHPUnit\Framework\TestCase {
+
+       use MediaWikiCoversValidator;
+
+       /**
+        * Returns objects that can serve as elements in the concrete
+        * GenericArrayObject deriving class being tested.
+        *
+        * @since 1.20
+        *
+        * @return array
+        */
+       abstract public function elementInstancesProvider();
+
+       /**
+        * Returns the name of the concrete class being tested.
+        *
+        * @since 1.20
+        *
+        * @return string
+        */
+       abstract public function getInstanceClass();
+
+       /**
+        * Provides instances of the concrete class being tested.
+        *
+        * @since 1.20
+        *
+        * @return array
+        */
+       public function instanceProvider() {
+               $instances = [];
+
+               foreach ( $this->elementInstancesProvider() as $elementInstances ) {
+                       $instances[] = $this->getNew( $elementInstances[0] );
+               }
+
+               return $this->arrayWrap( $instances );
+       }
+
+       /**
+        * @since 1.20
+        *
+        * @param array $elements
+        *
+        * @return GenericArrayObject
+        */
+       protected function getNew( array $elements = [] ) {
+               $class = $this->getInstanceClass();
+
+               return new $class( $elements );
+       }
+
+       /**
+        * @dataProvider elementInstancesProvider
+        *
+        * @since 1.20
+        *
+        * @param array $elements
+        *
+        * @covers GenericArrayObject::__construct
+        */
+       public function testConstructor( array $elements ) {
+               $arrayObject = $this->getNew( $elements );
+
+               $this->assertEquals( count( $elements ), $arrayObject->count() );
+       }
+
+       /**
+        * @dataProvider elementInstancesProvider
+        *
+        * @since 1.20
+        *
+        * @param array $elements
+        *
+        * @covers GenericArrayObject::isEmpty
+        */
+       public function testIsEmpty( array $elements ) {
+               $arrayObject = $this->getNew( $elements );
+
+               $this->assertEquals( $elements === [], $arrayObject->isEmpty() );
+       }
+
+       /**
+        * @dataProvider instanceProvider
+        *
+        * @since 1.20
+        *
+        * @param GenericArrayObject $list
+        *
+        * @covers GenericArrayObject::offsetUnset
+        */
+       public function testUnset( GenericArrayObject $list ) {
+               if ( $list->isEmpty() ) {
+                       $this->assertTrue( true ); // We cannot test unset if there are no elements
+               } else {
+                       $offset = $list->getIterator()->key();
+                       $count = $list->count();
+                       $list->offsetUnset( $offset );
+                       $this->assertEquals( $count - 1, $list->count() );
+               }
+
+               if ( !$list->isEmpty() ) {
+                       $offset = $list->getIterator()->key();
+                       $count = $list->count();
+                       unset( $list[$offset] );
+                       $this->assertEquals( $count - 1, $list->count() );
+               }
+       }
+
+       /**
+        * @dataProvider elementInstancesProvider
+        *
+        * @since 1.20
+        *
+        * @param array $elements
+        *
+        * @covers GenericArrayObject::append
+        */
+       public function testAppend( array $elements ) {
+               $list = $this->getNew();
+
+               $listSize = count( $elements );
+
+               foreach ( $elements as $element ) {
+                       $list->append( $element );
+               }
+
+               $this->assertEquals( $listSize, $list->count() );
+
+               $list = $this->getNew();
+
+               foreach ( $elements as $element ) {
+                       $list[] = $element;
+               }
+
+               $this->assertEquals( $listSize, $list->count() );
+
+               $this->checkTypeChecks( function ( GenericArrayObject $list, $element ) {
+                       $list->append( $element );
+               } );
+       }
+
+       /**
+        * @since 1.20
+        *
+        * @param callable $function
+        */
+       protected function checkTypeChecks( $function ) {
+               $excption = null;
+               $list = $this->getNew();
+
+               $elementClass = $list->getObjectType();
+
+               foreach ( [ 42, 'foo', [], new stdClass(), 4.2 ] as $element ) {
+                       $validValid = $element instanceof $elementClass;
+
+                       try {
+                               call_user_func( $function, $list, $element );
+                               $valid = true;
+                       } catch ( InvalidArgumentException $exception ) {
+                               $valid = false;
+                       }
+
+                       $this->assertEquals(
+                               $validValid,
+                               $valid,
+                               'Object of invalid type got successfully added to a GenericArrayObject'
+                       );
+               }
+       }
+
+       /**
+        * @dataProvider elementInstancesProvider
+        *
+        * @since 1.20
+        *
+        * @param array $elements
+        * @covers GenericArrayObject::getObjectType
+        * @covers GenericArrayObject::offsetSet
+        */
+       public function testOffsetSet( array $elements ) {
+               if ( $elements === [] ) {
+                       $this->assertTrue( true );
+
+                       return;
+               }
+
+               $list = $this->getNew();
+
+               $element = reset( $elements );
+               $list->offsetSet( 42, $element );
+               $this->assertEquals( $element, $list->offsetGet( 42 ) );
+
+               $list = $this->getNew();
+
+               $element = reset( $elements );
+               $list['oHai'] = $element;
+               $this->assertEquals( $element, $list['oHai'] );
+
+               $list = $this->getNew();
+
+               $element = reset( $elements );
+               $list->offsetSet( 9001, $element );
+               $this->assertEquals( $element, $list[9001] );
+
+               $list = $this->getNew();
+
+               $element = reset( $elements );
+               $list->offsetSet( null, $element );
+               $this->assertEquals( $element, $list[0] );
+
+               $list = $this->getNew();
+               $offset = 0;
+
+               foreach ( $elements as $element ) {
+                       $list->offsetSet( null, $element );
+                       $this->assertEquals( $element, $list[$offset++] );
+               }
+
+               $this->assertEquals( count( $elements ), $list->count() );
+
+               $this->checkTypeChecks( function ( GenericArrayObject $list, $element ) {
+                       $list->offsetSet( mt_rand(), $element );
+               } );
+       }
+
+       /**
+        * @dataProvider instanceProvider
+        *
+        * @since 1.21
+        *
+        * @param GenericArrayObject $list
+        *
+        * @covers GenericArrayObject::getSerializationData
+        * @covers GenericArrayObject::serialize
+        * @covers GenericArrayObject::unserialize
+        */
+       public function testSerialization( GenericArrayObject $list ) {
+               $serialization = serialize( $list );
+               $copy = unserialize( $serialization );
+
+               $this->assertEquals( $serialization, serialize( $copy ) );
+               $this->assertEquals( count( $list ), count( $copy ) );
+
+               $list = $list->getArrayCopy();
+               $copy = $copy->getArrayCopy();
+
+               $this->assertArrayEquals( $list, $copy, true, true );
+       }
+}
diff --git a/tests/phpunit/unit/includes/libs/HashRingTest.php b/tests/phpunit/unit/includes/libs/HashRingTest.php
new file mode 100644 (file)
index 0000000..acaeb02
--- /dev/null
@@ -0,0 +1,327 @@
+<?php
+
+/**
+ * @group HashRing
+ * @covers HashRing
+ */
+class HashRingTest extends PHPUnit\Framework\TestCase {
+
+       use MediaWikiCoversValidator;
+
+       public function testHashRingSerialize() {
+               $map = [ 's1' => 3, 's2' => 10, 's3' => 2, 's4' => 10, 's5' => 2, 's6' => 3 ];
+               $ring = new HashRing( $map, 'md5' );
+
+               $serialized = serialize( $ring );
+               $ringRemade = unserialize( $serialized );
+
+               for ( $i = 0; $i < 100; $i++ ) {
+                       $this->assertEquals(
+                               $ring->getLocation( "hello$i" ),
+                               $ringRemade->getLocation( "hello$i" ),
+                               'Items placed at proper locations'
+                       );
+               }
+       }
+
+       public function testHashRingMapping() {
+               // SHA-1 based and weighted
+               $ring = new HashRing(
+                       [ 's1' => 1, 's2' => 1, 's3' => 2, 's4' => 2, 's5' => 2, 's6' => 3, 's7' => 0 ],
+                       'sha1'
+               );
+
+               $this->assertEquals(
+                       [ 's1' => 1, 's2' => 1, 's3' => 2, 's4' => 2, 's5' => 2, 's6' => 3 ],
+                       $ring->getLocationWeights(),
+                       'Normalized location weights'
+               );
+
+               $locations = [];
+               for ( $i = 0; $i < 25; $i++ ) {
+                       $locations[ "hello$i"] = $ring->getLocation( "hello$i" );
+               }
+               $expectedLocations = [
+                       "hello0" => "s4",
+                       "hello1" => "s6",
+                       "hello2" => "s3",
+                       "hello3" => "s6",
+                       "hello4" => "s6",
+                       "hello5" => "s4",
+                       "hello6" => "s3",
+                       "hello7" => "s4",
+                       "hello8" => "s3",
+                       "hello9" => "s3",
+                       "hello10" => "s3",
+                       "hello11" => "s5",
+                       "hello12" => "s4",
+                       "hello13" => "s5",
+                       "hello14" => "s2",
+                       "hello15" => "s5",
+                       "hello16" => "s6",
+                       "hello17" => "s5",
+                       "hello18" => "s1",
+                       "hello19" => "s1",
+                       "hello20" => "s6",
+                       "hello21" => "s5",
+                       "hello22" => "s3",
+                       "hello23" => "s4",
+                       "hello24" => "s1"
+               ];
+               $this->assertEquals( $expectedLocations, $locations, 'Items placed at proper locations' );
+
+               $locations = [];
+               for ( $i = 0; $i < 5; $i++ ) {
+                       $locations[ "hello$i"] = $ring->getLocations( "hello$i", 2 );
+               }
+
+               $expectedLocations = [
+                       "hello0" => [ "s4", "s5" ],
+                       "hello1" => [ "s6", "s5" ],
+                       "hello2" => [ "s3", "s1" ],
+                       "hello3" => [ "s6", "s5" ],
+                       "hello4" => [ "s6", "s3" ],
+               ];
+               $this->assertEquals( $expectedLocations, $locations, 'Items placed at proper locations' );
+       }
+
+       /**
+        * @dataProvider providor_getHashLocationWeights
+        */
+       public function testHashRingRatios( $locations, $expectedHits ) {
+               $ring = new HashRing( $locations, 'whirlpool' );
+
+               $locationStats = array_fill_keys( array_keys( $locations ), 0 );
+               for ( $i = 0; $i < 10000; ++$i ) {
+                       ++$locationStats[$ring->getLocation( "key-$i" )];
+               }
+               $this->assertEquals( $expectedHits, $locationStats );
+       }
+
+       public static function providor_getHashLocationWeights() {
+               return [
+                       [
+                               [ 'big' => 10, 'medium' => 5, 'small' => 1 ],
+                               [ 'big' => 6037, 'medium' => 3314, 'small' => 649 ]
+                       ]
+               ];
+       }
+
+       /**
+        * @dataProvider providor_getHashLocationWeights2
+        */
+       public function testHashRingRatios2( $locations, $expected ) {
+               $ring = new HashRing( $locations, 'sha1' );
+               $locationStats = array_fill_keys( array_keys( $locations ), 0 );
+               for ( $i = 0; $i < 1000; ++$i ) {
+                       foreach ( $ring->getLocations( "key-$i", 3 ) as $location ) {
+                               ++$locationStats[$location];
+                       }
+               }
+               $this->assertEquals( $expected, $locationStats );
+       }
+
+       public static function providor_getHashLocationWeights2() {
+               return [
+                       [
+                               [ 'big1' => 10, 'big2' => 10, 'big3' => 10, 'small1' => 1, 'small2' => 1 ],
+                               [ 'big1' => 929, 'big2' => 899, 'big3' => 887, 'small1' => 143, 'small2' => 142 ]
+                       ]
+               ];
+       }
+
+       public function testHashRingEjection() {
+               $map = [ 's1' => 5, 's2' => 5, 's3' => 10, 's4' => 10, 's5' => 5, 's6' => 5 ];
+               $ring = new HashRing( $map, 'md5' );
+
+               $ring->ejectFromLiveRing( 's3', 30 );
+               $ring->ejectFromLiveRing( 's6', 15 );
+
+               $this->assertEquals(
+                       [ 's1' => 5, 's2' => 5, 's4' => 10, 's5' => 5 ],
+                       $ring->getLiveLocationWeights(),
+                       'Live location weights'
+               );
+
+               for ( $i = 0; $i < 100; ++$i ) {
+                       $key = "key-$i";
+
+                       $this->assertNotEquals( 's3', $ring->getLiveLocation( $key ), 'ejected' );
+                       $this->assertNotEquals( 's6', $ring->getLiveLocation( $key ), 'ejected' );
+
+                       if ( !in_array( $ring->getLocation( $key ), [ 's3', 's6' ], true ) ) {
+                               $this->assertEquals(
+                                       $ring->getLocation( $key ),
+                                       $ring->getLiveLocation( $key ),
+                                       "Live ring otherwise matches (#$i)"
+                               );
+                               $this->assertEquals(
+                                       $ring->getLocations( $key, 1 ),
+                                       $ring->getLiveLocations( $key, 1 ),
+                                       "Live ring otherwise matches (#$i)"
+                               );
+                       }
+               }
+       }
+
+       public function testHashRingCollision() {
+               $ring1 = new HashRing( [ 0 => 1, 6497 => 1 ] );
+               $ring2 = new HashRing( [ 6497 => 1, 0 => 1 ] );
+
+               for ( $i = 0; $i < 100; ++$i ) {
+                       $this->assertEquals( $ring1->getLocation( $i ), $ring2->getLocation( $i ) );
+               }
+       }
+
+       public function testHashRingKetamaMode() {
+               // Same as https://github.com/RJ/ketama/blob/master/ketama.servers
+               $map = [
+                       '10.0.1.1:11211' => 600,
+                       '10.0.1.2:11211' => 300,
+                       '10.0.1.3:11211' => 200,
+                       '10.0.1.4:11211' => 350,
+                       '10.0.1.5:11211' => 1000,
+                       '10.0.1.6:11211' => 800,
+                       '10.0.1.7:11211' => 950,
+                       '10.0.1.8:11211' => 100
+               ];
+               $ring = new HashRing( $map, 'md5' );
+               $wrapper = \Wikimedia\TestingAccessWrapper::newFromObject( $ring );
+
+               $ketama_test = function ( $count ) use ( $wrapper ) {
+                       $baseRing = $wrapper->baseRing;
+
+                       $lines = [];
+                       for ( $key = 0; $key < $count; ++$key ) {
+                               $location = $wrapper->getLocation( $key );
+
+                               $itemPos = $wrapper->getItemPosition( $key );
+                               $nodeIndex = $wrapper->findNodeIndexForPosition( $itemPos, $baseRing );
+                               $nodePos = $baseRing[$nodeIndex][HashRing::KEY_POS];
+
+                               $lines[] = sprintf( "%u %u %s\n", $itemPos, $nodePos, $location );
+                       }
+
+                       return "\n" . implode( '', $lines );
+               };
+
+               // Known correct values generated from C code:
+               // https://github.com/RJ/ketama/blob/master/libketama/ketama_test.c
+               $expected = <<<EOT
+
+2216742351 2217271743 10.0.1.1:11211
+943901380 949045552 10.0.1.5:11211
+2373066440 2374693370 10.0.1.6:11211
+2127088620 2130338203 10.0.1.6:11211
+2046197672 2051996197 10.0.1.7:11211
+2134629092 2135172435 10.0.1.1:11211
+470382870 472541453 10.0.1.7:11211
+1608782991 1609789509 10.0.1.3:11211
+2516119753 2520092206 10.0.1.2:11211
+3465331781 3466294492 10.0.1.4:11211
+1749342675 1753760600 10.0.1.5:11211
+1136464485 1137779711 10.0.1.1:11211
+3620997826 3621580689 10.0.1.7:11211
+283385029 285581365 10.0.1.6:11211
+2300818346 2302165654 10.0.1.5:11211
+2132603803 2134614475 10.0.1.8:11211
+2962705863 2969767984 10.0.1.2:11211
+786427760 786565633 10.0.1.5:11211
+4095887727 4096760944 10.0.1.6:11211
+2906459679 2906987515 10.0.1.6:11211
+137884056 138922607 10.0.1.4:11211
+81549628 82491298 10.0.1.6:11211
+3530020790 3530525869 10.0.1.6:11211
+4231817527 4234960467 10.0.1.7:11211
+2011099423 2014738083 10.0.1.7:11211
+107620750 120968799 10.0.1.6:11211
+3979113294 3981926993 10.0.1.4:11211
+273671938 276355738 10.0.1.4:11211
+4032816947 4033300359 10.0.1.5:11211
+464234862 466093615 10.0.1.1:11211
+3007059764 3007671127 10.0.1.5:11211
+542337729 542491760 10.0.1.7:11211
+4040385635 4044064727 10.0.1.5:11211
+3319802648 3320661601 10.0.1.7:11211
+1032153571 1035085391 10.0.1.1:11211
+3543939100 3545608820 10.0.1.5:11211
+3876899353 3885324049 10.0.1.2:11211
+3771318181 3773259708 10.0.1.8:11211
+3457906597 3459285639 10.0.1.5:11211
+3028975062 3031083168 10.0.1.7:11211
+244467158 250943416 10.0.1.5:11211
+1604785716 1609789509 10.0.1.3:11211
+3905343649 3905751132 10.0.1.1:11211
+1713497623 1725056963 10.0.1.5:11211
+1668356087 1668827816 10.0.1.5:11211
+3427369836 3438933308 10.0.1.1:11211
+2515850457 2520092206 10.0.1.2:11211
+3886138983 3887390208 10.0.1.1:11211
+4019334756 4023153300 10.0.1.8:11211
+1170561012 1170785765 10.0.1.7:11211
+1841809344 1848425105 10.0.1.6:11211
+973223976 973369204 10.0.1.1:11211
+358093210 359562433 10.0.1.6:11211
+378350808 380841931 10.0.1.5:11211
+4008477862 4012085095 10.0.1.7:11211
+1027226549 1028630030 10.0.1.6:11211
+2386583967 2387706118 10.0.1.1:11211
+522892146 524831677 10.0.1.7:11211
+3779194982 3788912803 10.0.1.5:11211
+3764731657 3771312500 10.0.1.7:11211
+184756999 187529415 10.0.1.6:11211
+838351231 845886003 10.0.1.3:11211
+2827220548 2828019973 10.0.1.6:11211
+3604721411 3607668249 10.0.1.6:11211
+472866282 475506254 10.0.1.5:11211
+2752268796 2754833471 10.0.1.5:11211
+1791464754 1795042583 10.0.1.7:11211
+3029359475 3031083168 10.0.1.7:11211
+3633378211 3639985542 10.0.1.6:11211
+3148267284 3149217023 10.0.1.6:11211
+163887996 166705043 10.0.1.7:11211
+3642803426 3649125922 10.0.1.7:11211
+3901799218 3902199881 10.0.1.7:11211
+418045394 425867331 10.0.1.6:11211
+346775981 348578169 10.0.1.6:11211
+368352208 372224616 10.0.1.7:11211
+2643711995 2644259911 10.0.1.5:11211
+2032983336 2033860601 10.0.1.6:11211
+3567842357 3572867530 10.0.1.2:11211
+1024982737 1028630030 10.0.1.6:11211
+933966832 938106828 10.0.1.7:11211
+2102520899 2103402846 10.0.1.7:11211
+3537205399 3538094881 10.0.1.7:11211
+2311233534 2314593262 10.0.1.1:11211
+2500514664 2503565236 10.0.1.7:11211
+1091958846 1093484995 10.0.1.6:11211
+3984972691 3987453644 10.0.1.1:11211
+2669994439 2670911201 10.0.1.4:11211
+2846111786 2846115813 10.0.1.5:11211
+1805010806 1808593732 10.0.1.8:11211
+1587024774 1587746378 10.0.1.5:11211
+3214549588 3215619351 10.0.1.2:11211
+1965214866 1970922428 10.0.1.7:11211
+1038671000 1040777775 10.0.1.7:11211
+820820468 823114475 10.0.1.6:11211
+2722835329 2723166435 10.0.1.5:11211
+1602053414 1604196066 10.0.1.5:11211
+1330835426 1335097278 10.0.1.5:11211
+556547565 557075710 10.0.1.4:11211
+2977587884 2978402952 10.0.1.1:11211
+
+EOT;
+
+               $this->assertEquals( $expected, $ketama_test( 100 ), 'Ketama mode (diff check)' );
+
+               // Hash of known correct values from C code
+               $this->assertEquals(
+                       'c69ac9eb7a8a630c0cded201cefeaace',
+                       md5( $ketama_test( 1e5 ) ),
+                       'Ketama mode (large, MD5 check)'
+               );
+
+               // Slower, full upstream MD5 check, manually verified 3/21/2018
+               // $this->assertEquals( '5672b131391f5aa2b280936aec1eea74', md5( $ketama_test( 1e6 ) ) );
+       }
+}
diff --git a/tests/phpunit/unit/includes/libs/HtmlArmorTest.php b/tests/phpunit/unit/includes/libs/HtmlArmorTest.php
new file mode 100644 (file)
index 0000000..c5e87e4
--- /dev/null
@@ -0,0 +1,55 @@
+<?php
+
+/**
+ * @covers HtmlArmor
+ */
+class HtmlArmorTest extends PHPUnit\Framework\TestCase {
+
+       use MediaWikiCoversValidator;
+
+       public static function provideConstructor() {
+               return [
+                       [ 'test' ],
+                       [ null ],
+                       [ '<em>some html!</em>' ]
+               ];
+       }
+
+       /**
+        * @dataProvider provideConstructor
+        */
+       public function testConstructor( $value ) {
+               $this->assertInstanceOf( HtmlArmor::class, new HtmlArmor( $value ) );
+       }
+
+       public static function provideGetHtml() {
+               return [
+                       [
+                               'foobar',
+                               'foobar',
+                       ],
+                       [
+                               '<script>alert("evil!");</script>',
+                               '&lt;script&gt;alert(&quot;evil!&quot;);&lt;/script&gt;',
+                       ],
+                       [
+                               new HtmlArmor( '<script>alert("evil!");</script>' ),
+                               '<script>alert("evil!");</script>',
+                       ],
+                       [
+                               new HtmlArmor( null ),
+                               null,
+                       ]
+               ];
+       }
+
+       /**
+        * @dataProvider provideGetHtml
+        */
+       public function testGetHtml( $input, $expected ) {
+               $this->assertEquals(
+                       $expected,
+                       HtmlArmor::getHtml( $input )
+               );
+       }
+}
diff --git a/tests/phpunit/unit/includes/libs/IEUrlExtensionTest.php b/tests/phpunit/unit/includes/libs/IEUrlExtensionTest.php
new file mode 100644 (file)
index 0000000..e04b2e2
--- /dev/null
@@ -0,0 +1,42 @@
+<?php
+
+class IEUrlExtensionTest extends PHPUnit\Framework\TestCase {
+
+       use MediaWikiCoversValidator;
+
+       public function provideFindIE6Extension() {
+               return [
+                       // url, expected, message
+                       [ 'x.y', 'y', 'Simple extension' ],
+                       [ 'x', '', 'No extension' ],
+                       [ '', '', 'Empty string' ],
+                       [ '?', '', 'Question mark only' ],
+                       [ '.x?', 'x', 'Extension then question mark' ],
+                       [ '?.x', 'x', 'Question mark then extension' ],
+                       [ '.x*', '', 'Extension with invalid character' ],
+                       [ '*.x', 'x', 'Invalid character followed by an extension' ],
+                       [ 'a?b?.c?.d?e?f', 'c', 'Multiple question marks' ],
+                       [ 'a?b?.exe?.d?.e', 'd', '.exe exception' ],
+                       [ 'a?b?.exe', 'exe', '.exe exception 2' ],
+                       [ 'a#b.c', '', 'Hash character preceding extension' ],
+                       [ 'a?#b.c', '', 'Hash character preceding extension 2' ],
+                       [ '.', '', 'Dot at end of string' ],
+                       [ 'x.y.z', 'z', 'Two dots' ],
+                       [ 'example.php?foo=a&bar=b', 'php', 'Script with query' ],
+                       [ 'example%2Ephp?foo=a&bar=b', '', 'Script with urlencoded dot and query' ],
+                       [ 'example%2Ephp?foo=a.x&bar=b.y', 'y', 'Script with urlencoded dot and query with dot' ],
+               ];
+       }
+
+       /**
+        * @covers IEUrlExtension::findIE6Extension
+        * @dataProvider provideFindIE6Extension
+        */
+       public function testFindIE6Extension( $url, $expected, $message ) {
+               $this->assertEquals(
+                       $expected,
+                       IEUrlExtension::findIE6Extension( $url ),
+                       $message
+               );
+       }
+}
diff --git a/tests/phpunit/unit/includes/libs/IPTest.php b/tests/phpunit/unit/includes/libs/IPTest.php
new file mode 100644 (file)
index 0000000..9ec53c0
--- /dev/null
@@ -0,0 +1,673 @@
+<?php
+/**
+ * Tests for IP validity functions.
+ *
+ * Ported from /t/inc/IP.t by avar.
+ *
+ * @group IP
+ * @todo Test methods in this call should be split into a method and a
+ * dataprovider.
+ */
+class IPTest extends PHPUnit\Framework\TestCase {
+
+       use MediaWikiCoversValidator;
+
+       /**
+        * @covers IP::isIPAddress
+        * @dataProvider provideInvalidIPs
+        */
+       public function testIsNotIPAddress( $val, $desc ) {
+               $this->assertFalse( IP::isIPAddress( $val ), $desc );
+       }
+
+       /**
+        * Provide a list of things that aren't IP addresses
+        */
+       public function provideInvalidIPs() {
+               return [
+                       [ false, 'Boolean false is not an IP' ],
+                       [ true, 'Boolean true is not an IP' ],
+                       [ '', 'Empty string is not an IP' ],
+                       [ 'abc', 'Garbage IP string' ],
+                       [ ':', 'Single ":" is not an IP' ],
+                       [ '2001:0DB8::A:1::1', 'IPv6 with a double :: occurrence' ],
+                       [ '2001:0DB8::A:1::', 'IPv6 with a double :: occurrence, last at end' ],
+                       [ '::2001:0DB8::5:1', 'IPv6 with a double :: occurrence, firt at beginning' ],
+                       [ '124.24.52', 'IPv4 not enough quads' ],
+                       [ '24.324.52.13', 'IPv4 out of range' ],
+                       [ '.24.52.13', 'IPv4 starts with period' ],
+                       [ 'fc:100:300', 'IPv6 with only 3 words' ],
+               ];
+       }
+
+       /**
+        * @covers IP::isIPAddress
+        */
+       public function testisIPAddress() {
+               $this->assertTrue( IP::isIPAddress( '::' ), 'RFC 4291 IPv6 Unspecified Address' );
+               $this->assertTrue( IP::isIPAddress( '::1' ), 'RFC 4291 IPv6 Loopback Address' );
+               $this->assertTrue( IP::isIPAddress( '74.24.52.13/20' ), 'IPv4 range' );
+               $this->assertTrue( IP::isIPAddress( 'fc:100:a:d:1:e:ac:0/24' ), 'IPv6 range' );
+               $this->assertTrue( IP::isIPAddress( 'fc::100:a:d:1:e:ac/96' ), 'IPv6 range with "::"' );
+
+               $validIPs = [ 'fc:100::', 'fc:100:a:d:1:e:ac::', 'fc::100', '::fc:100:a:d:1:e:ac',
+                       '::fc', 'fc::100:a:d:1:e:ac', 'fc:100:a:d:1:e:ac:0', '124.24.52.13', '1.24.52.13' ];
+               foreach ( $validIPs as $ip ) {
+                       $this->assertTrue( IP::isIPAddress( $ip ), "$ip is a valid IP address" );
+               }
+       }
+
+       /**
+        * @covers IP::isIPv6
+        */
+       public function testisIPv6() {
+               $this->assertFalse( IP::isIPv6( ':fc:100::' ), 'IPv6 starting with lone ":"' );
+               $this->assertFalse( IP::isIPv6( 'fc:100:::' ), 'IPv6 ending with a ":::"' );
+               $this->assertFalse( IP::isIPv6( 'fc:300' ), 'IPv6 with only 2 words' );
+               $this->assertFalse( IP::isIPv6( 'fc:100:300' ), 'IPv6 with only 3 words' );
+
+               $this->assertTrue( IP::isIPv6( 'fc:100::' ) );
+               $this->assertTrue( IP::isIPv6( 'fc:100:a::' ) );
+               $this->assertTrue( IP::isIPv6( 'fc:100:a:d::' ) );
+               $this->assertTrue( IP::isIPv6( 'fc:100:a:d:1::' ) );
+               $this->assertTrue( IP::isIPv6( 'fc:100:a:d:1:e::' ) );
+               $this->assertTrue( IP::isIPv6( 'fc:100:a:d:1:e:ac::' ) );
+
+               $this->assertFalse( IP::isIPv6( 'fc:100:a:d:1:e:ac:0::' ), 'IPv6 with 8 words ending with "::"' );
+               $this->assertFalse(
+                       IP::isIPv6( 'fc:100:a:d:1:e:ac:0:1::' ),
+                       'IPv6 with 9 words ending with "::"'
+               );
+
+               $this->assertFalse( IP::isIPv6( ':::' ) );
+               $this->assertFalse( IP::isIPv6( '::0:' ), 'IPv6 ending in a lone ":"' );
+
+               $this->assertTrue( IP::isIPv6( '::' ), 'IPv6 zero address' );
+               $this->assertTrue( IP::isIPv6( '::0' ) );
+               $this->assertTrue( IP::isIPv6( '::fc' ) );
+               $this->assertTrue( IP::isIPv6( '::fc:100' ) );
+               $this->assertTrue( IP::isIPv6( '::fc:100:a' ) );
+               $this->assertTrue( IP::isIPv6( '::fc:100:a:d' ) );
+               $this->assertTrue( IP::isIPv6( '::fc:100:a:d:1' ) );
+               $this->assertTrue( IP::isIPv6( '::fc:100:a:d:1:e' ) );
+               $this->assertTrue( IP::isIPv6( '::fc:100:a:d:1:e:ac' ) );
+
+               $this->assertFalse( IP::isIPv6( '::fc:100:a:d:1:e:ac:0' ), 'IPv6 with "::" and 8 words' );
+               $this->assertFalse( IP::isIPv6( '::fc:100:a:d:1:e:ac:0:1' ), 'IPv6 with 9 words' );
+
+               $this->assertFalse( IP::isIPv6( ':fc::100' ), 'IPv6 starting with lone ":"' );
+               $this->assertFalse( IP::isIPv6( 'fc::100:' ), 'IPv6 ending with lone ":"' );
+               $this->assertFalse( IP::isIPv6( 'fc:::100' ), 'IPv6 with ":::" in the middle' );
+
+               $this->assertTrue( IP::isIPv6( 'fc::100' ), 'IPv6 with "::" and 2 words' );
+               $this->assertTrue( IP::isIPv6( 'fc::100:a' ), 'IPv6 with "::" and 3 words' );
+               $this->assertTrue( IP::isIPv6( 'fc::100:a:d' ), 'IPv6 with "::" and 4 words' );
+               $this->assertTrue( IP::isIPv6( 'fc::100:a:d:1' ), 'IPv6 with "::" and 5 words' );
+               $this->assertTrue( IP::isIPv6( 'fc::100:a:d:1:e' ), 'IPv6 with "::" and 6 words' );
+               $this->assertTrue( IP::isIPv6( 'fc::100:a:d:1:e:ac' ), 'IPv6 with "::" and 7 words' );
+               $this->assertTrue( IP::isIPv6( '2001::df' ), 'IPv6 with "::" and 2 words' );
+               $this->assertTrue( IP::isIPv6( '2001:5c0:1400:a::df' ), 'IPv6 with "::" and 5 words' );
+               $this->assertTrue( IP::isIPv6( '2001:5c0:1400:a::df:2' ), 'IPv6 with "::" and 6 words' );
+
+               $this->assertFalse( IP::isIPv6( 'fc::100:a:d:1:e:ac:0' ), 'IPv6 with "::" and 8 words' );
+               $this->assertFalse( IP::isIPv6( 'fc::100:a:d:1:e:ac:0:1' ), 'IPv6 with 9 words' );
+
+               $this->assertTrue( IP::isIPv6( 'fc:100:a:d:1:e:ac:0' ) );
+       }
+
+       /**
+        * @covers IP::isIPv4
+        * @dataProvider provideInvalidIPv4Addresses
+        */
+       public function testisNotIPv4( $bogusIP, $desc ) {
+               $this->assertFalse( IP::isIPv4( $bogusIP ), $desc );
+       }
+
+       public function provideInvalidIPv4Addresses() {
+               return [
+                       [ false, 'Boolean false is not an IP' ],
+                       [ true, 'Boolean true is not an IP' ],
+                       [ '', 'Empty string is not an IP' ],
+                       [ 'abc', 'Letters are not an IP' ],
+                       [ ':', 'A colon is not an IP' ],
+                       [ '124.24.52', 'IPv4 not enough quads' ],
+                       [ '24.324.52.13', 'IPv4 out of range' ],
+                       [ '.24.52.13', 'IPv4 starts with period' ],
+               ];
+       }
+
+       /**
+        * @covers IP::isIPv4
+        * @dataProvider provideValidIPv4Address
+        */
+       public function testIsIPv4( $ip, $desc ) {
+               $this->assertTrue( IP::isIPv4( $ip ), $desc );
+       }
+
+       /**
+        * Provide some IPv4 addresses and ranges
+        */
+       public function provideValidIPv4Address() {
+               return [
+                       [ '124.24.52.13', 'Valid IPv4 address' ],
+                       [ '1.24.52.13', 'Another valid IPv4 address' ],
+                       [ '74.24.52.13/20', 'An IPv4 range' ],
+               ];
+       }
+
+       /**
+        * @covers IP::isValid
+        */
+       public function testValidIPs() {
+               foreach ( range( 0, 255 ) as $i ) {
+                       $a = sprintf( "%03d", $i );
+                       $b = sprintf( "%02d", $i );
+                       $c = sprintf( "%01d", $i );
+                       foreach ( array_unique( [ $a, $b, $c ] ) as $f ) {
+                               $ip = "$f.$f.$f.$f";
+                               $this->assertTrue( IP::isValid( $ip ), "$ip is a valid IPv4 address" );
+                       }
+               }
+               foreach ( range( 0x0, 0xFFFF, 0xF ) as $i ) {
+                       $a = sprintf( "%04x", $i );
+                       $b = sprintf( "%03x", $i );
+                       $c = sprintf( "%02x", $i );
+                       foreach ( array_unique( [ $a, $b, $c ] ) as $f ) {
+                               $ip = "$f:$f:$f:$f:$f:$f:$f:$f";
+                               $this->assertTrue( IP::isValid( $ip ), "$ip is a valid IPv6 address" );
+                       }
+               }
+               // test with some abbreviations
+               $this->assertFalse( IP::isValid( ':fc:100::' ), 'IPv6 starting with lone ":"' );
+               $this->assertFalse( IP::isValid( 'fc:100:::' ), 'IPv6 ending with a ":::"' );
+               $this->assertFalse( IP::isValid( 'fc:300' ), 'IPv6 with only 2 words' );
+               $this->assertFalse( IP::isValid( 'fc:100:300' ), 'IPv6 with only 3 words' );
+
+               $this->assertTrue( IP::isValid( 'fc:100::' ) );
+               $this->assertTrue( IP::isValid( 'fc:100:a:d:1:e::' ) );
+               $this->assertTrue( IP::isValid( 'fc:100:a:d:1:e:ac::' ) );
+
+               $this->assertTrue( IP::isValid( 'fc::100' ), 'IPv6 with "::" and 2 words' );
+               $this->assertTrue( IP::isValid( 'fc::100:a' ), 'IPv6 with "::" and 3 words' );
+               $this->assertTrue( IP::isValid( '2001::df' ), 'IPv6 with "::" and 2 words' );
+               $this->assertTrue( IP::isValid( '2001:5c0:1400:a::df' ), 'IPv6 with "::" and 5 words' );
+               $this->assertTrue( IP::isValid( '2001:5c0:1400:a::df:2' ), 'IPv6 with "::" and 6 words' );
+               $this->assertTrue( IP::isValid( 'fc::100:a:d:1' ), 'IPv6 with "::" and 5 words' );
+               $this->assertTrue( IP::isValid( 'fc::100:a:d:1:e:ac' ), 'IPv6 with "::" and 7 words' );
+
+               $this->assertFalse(
+                       IP::isValid( 'fc:100:a:d:1:e:ac:0::' ),
+                       'IPv6 with 8 words ending with "::"'
+               );
+               $this->assertFalse(
+                       IP::isValid( 'fc:100:a:d:1:e:ac:0:1::' ),
+                       'IPv6 with 9 words ending with "::"'
+               );
+       }
+
+       /**
+        * @covers IP::isValid
+        */
+       public function testInvalidIPs() {
+               // Out of range...
+               foreach ( range( 256, 999 ) as $i ) {
+                       $a = sprintf( "%03d", $i );
+                       $b = sprintf( "%02d", $i );
+                       $c = sprintf( "%01d", $i );
+                       foreach ( array_unique( [ $a, $b, $c ] ) as $f ) {
+                               $ip = "$f.$f.$f.$f";
+                               $this->assertFalse( IP::isValid( $ip ), "$ip is not a valid IPv4 address" );
+                       }
+               }
+               foreach ( range( 'g', 'z' ) as $i ) {
+                       $a = sprintf( "%04s", $i );
+                       $b = sprintf( "%03s", $i );
+                       $c = sprintf( "%02s", $i );
+                       foreach ( array_unique( [ $a, $b, $c ] ) as $f ) {
+                               $ip = "$f:$f:$f:$f:$f:$f:$f:$f";
+                               $this->assertFalse( IP::isValid( $ip ), "$ip is not a valid IPv6 address" );
+                       }
+               }
+               // Have CIDR
+               $ipCIDRs = [
+                       '212.35.31.121/32',
+                       '212.35.31.121/18',
+                       '212.35.31.121/24',
+                       '::ff:d:321:5/96',
+                       'ff::d3:321:5/116',
+                       'c:ff:12:1:ea:d:321:5/120',
+               ];
+               foreach ( $ipCIDRs as $i ) {
+                       $this->assertFalse( IP::isValid( $i ),
+                               "$i is an invalid IP address because it is a range" );
+               }
+               // Incomplete/garbage
+               $invalid = [
+                       'www.xn--var-xla.net',
+                       '216.17.184.G',
+                       '216.17.184.1.',
+                       '216.17.184',
+                       '216.17.184.',
+                       '256.17.184.1'
+               ];
+               foreach ( $invalid as $i ) {
+                       $this->assertFalse( IP::isValid( $i ), "$i is an invalid IP address" );
+               }
+       }
+
+       /**
+        * Provide some valid IP ranges
+        */
+       public function provideValidRanges() {
+               return [
+                       [ '116.17.184.5/32' ],
+                       [ '0.17.184.5/30' ],
+                       [ '16.17.184.1/24' ],
+                       [ '30.242.52.14/1' ],
+                       [ '10.232.52.13/8' ],
+                       [ '30.242.52.14/0' ],
+                       [ '::e:f:2001/96' ],
+                       [ '::c:f:2001/128' ],
+                       [ '::10:f:2001/70' ],
+                       [ '::fe:f:2001/1' ],
+                       [ '::6d:f:2001/8' ],
+                       [ '::fe:f:2001/0' ],
+               ];
+       }
+
+       /**
+        * @covers IP::isValidRange
+        * @dataProvider provideValidRanges
+        */
+       public function testValidRanges( $range ) {
+               $this->assertTrue( IP::isValidRange( $range ), "$range is a valid IP range" );
+       }
+
+       /**
+        * @covers IP::isValidRange
+        * @dataProvider provideInvalidRanges
+        */
+       public function testInvalidRanges( $invalid ) {
+               $this->assertFalse( IP::isValidRange( $invalid ), "$invalid is not a valid IP range" );
+       }
+
+       public function provideInvalidRanges() {
+               return [
+                       [ '116.17.184.5/33' ],
+                       [ '0.17.184.5/130' ],
+                       [ '16.17.184.1/-1' ],
+                       [ '10.232.52.13/*' ],
+                       [ '7.232.52.13/ab' ],
+                       [ '11.232.52.13/' ],
+                       [ '::e:f:2001/129' ],
+                       [ '::c:f:2001/228' ],
+                       [ '::10:f:2001/-1' ],
+                       [ '::6d:f:2001/*' ],
+                       [ '::86:f:2001/ab' ],
+                       [ '::23:f:2001/' ],
+               ];
+       }
+
+       /**
+        * @covers IP::sanitizeIP
+        * @dataProvider provideSanitizeIP
+        */
+       public function testSanitizeIP( $expected, $input ) {
+               $result = IP::sanitizeIP( $input );
+               $this->assertEquals( $expected, $result );
+       }
+
+       /**
+        * Provider for IP::testSanitizeIP()
+        */
+       public static function provideSanitizeIP() {
+               return [
+                       [ '0.0.0.0', '0.0.0.0' ],
+                       [ '0.0.0.0', '00.00.00.00' ],
+                       [ '0.0.0.0', '000.000.000.000' ],
+                       [ '0.0.0.0/24', '000.000.000.000/24' ],
+                       [ '141.0.11.253', '141.000.011.253' ],
+                       [ '1.2.4.5', '1.2.4.5' ],
+                       [ '1.2.4.5', '01.02.04.05' ],
+                       [ '1.2.4.5', '001.002.004.005' ],
+                       [ '10.0.0.1', '010.0.000.1' ],
+                       [ '80.72.250.4', '080.072.250.04' ],
+                       [ 'Foo.1000.00', 'Foo.1000.00' ],
+                       [ 'Bar.01', 'Bar.01' ],
+                       [ 'Bar.010', 'Bar.010' ],
+                       [ null, '' ],
+                       [ null, ' ' ]
+               ];
+       }
+
+       /**
+        * @covers IP::toHex
+        * @dataProvider provideToHex
+        */
+       public function testToHex( $expected, $input ) {
+               $result = IP::toHex( $input );
+               $this->assertTrue( $result === false || is_string( $result ) );
+               $this->assertEquals( $expected, $result );
+       }
+
+       /**
+        * Provider for IP::testToHex()
+        */
+       public static function provideToHex() {
+               return [
+                       [ '00000001', '0.0.0.1' ],
+                       [ '01020304', '1.2.3.4' ],
+                       [ '7F000001', '127.0.0.1' ],
+                       [ '80000000', '128.0.0.0' ],
+                       [ 'DEADCAFE', '222.173.202.254' ],
+                       [ 'FFFFFFFF', '255.255.255.255' ],
+                       [ '8D000BFD', '141.000.11.253' ],
+                       [ false, 'IN.VA.LI.D' ],
+                       [ 'v6-00000000000000000000000000000001', '::1' ],
+                       [ 'v6-20010DB885A3000000008A2E03707334', '2001:0db8:85a3:0000:0000:8a2e:0370:7334' ],
+                       [ 'v6-20010DB885A3000000008A2E03707334', '2001:db8:85a3::8a2e:0370:7334' ],
+                       [ false, 'IN:VA::LI:D' ],
+                       [ false, ':::1' ]
+               ];
+       }
+
+       /**
+        * @covers IP::isPublic
+        * @dataProvider provideIsPublic
+        */
+       public function testIsPublic( $expected, $input ) {
+               $result = IP::isPublic( $input );
+               $this->assertEquals( $expected, $result );
+       }
+
+       /**
+        * Provider for IP::testIsPublic()
+        */
+       public static function provideIsPublic() {
+               return [
+                       [ false, 'fc00::3' ], # RFC 4193 (local)
+                       [ false, 'fc00::ff' ], # RFC 4193 (local)
+                       [ false, '127.1.2.3' ], # loopback
+                       [ false, '::1' ], # loopback
+                       [ false, 'fe80::1' ], # link-local
+                       [ false, '169.254.1.1' ], # link-local
+                       [ false, '10.0.0.1' ], # RFC 1918 (private)
+                       [ false, '172.16.0.1' ], # RFC 1918 (private)
+                       [ false, '192.168.0.1' ], # RFC 1918 (private)
+                       [ true, '2001:5c0:1000:a::133' ], # public
+                       [ true, 'fc::3' ], # public
+                       [ true, '00FC::' ] # public
+               ];
+       }
+
+       // Private wrapper used to test CIDR Parsing.
+       private function assertFalseCIDR( $CIDR, $msg = '' ) {
+               $ff = [ false, false ];
+               $this->assertEquals( $ff, IP::parseCIDR( $CIDR ), $msg );
+       }
+
+       // Private wrapper to test network shifting using only dot notation
+       private function assertNet( $expected, $CIDR ) {
+               $parse = IP::parseCIDR( $CIDR );
+               $this->assertEquals( $expected, long2ip( $parse[0] ), "network shifting $CIDR" );
+       }
+
+       /**
+        * @covers IP::hexToQuad
+        * @dataProvider provideIPsAndHexes
+        */
+       public function testHexToQuad( $ip, $hex ) {
+               $this->assertEquals( $ip, IP::hexToQuad( $hex ) );
+       }
+
+       /**
+        * Provide some IP addresses and their equivalent hex representations
+        */
+       public function provideIPsandHexes() {
+               return [
+                       [ '0.0.0.1', '00000001' ],
+                       [ '255.0.0.0', 'FF000000' ],
+                       [ '255.255.255.255', 'FFFFFFFF' ],
+                       [ '10.188.222.255', '0ABCDEFF' ],
+                       // hex not left-padded...
+                       [ '0.0.0.0', '0' ],
+                       [ '0.0.0.1', '1' ],
+                       [ '0.0.0.255', 'FF' ],
+                       [ '0.0.255.0', 'FF00' ],
+               ];
+       }
+
+       /**
+        * @covers IP::hexToOctet
+        * @dataProvider provideOctetsAndHexes
+        */
+       public function testHexToOctet( $octet, $hex ) {
+               $this->assertEquals( $octet, IP::hexToOctet( $hex ) );
+       }
+
+       /**
+        * Provide some hex and octet representations of the same IPs
+        */
+       public function provideOctetsAndHexes() {
+               return [
+                       [ '0:0:0:0:0:0:0:1', '00000000000000000000000000000001' ],
+                       [ '0:0:0:0:0:0:FF:3', '00000000000000000000000000FF0003' ],
+                       [ '0:0:0:0:0:0:FF00:6', '000000000000000000000000FF000006' ],
+                       [ '0:0:0:0:0:0:FCCF:FAFF', '000000000000000000000000FCCFFAFF' ],
+                       [ 'FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF', 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF' ],
+                       // hex not left-padded...
+                       [ '0:0:0:0:0:0:0:0', '0' ],
+                       [ '0:0:0:0:0:0:0:1', '1' ],
+                       [ '0:0:0:0:0:0:0:FF', 'FF' ],
+                       [ '0:0:0:0:0:0:0:FFD0', 'FFD0' ],
+                       [ '0:0:0:0:0:0:FA00:0', 'FA000000' ],
+                       [ '0:0:0:0:0:0:FCCF:FAFF', 'FCCFFAFF' ],
+               ];
+       }
+
+       /**
+        * IP::parseCIDR() returns an array containing a signed IP address
+        * representing the network mask and the bit mask.
+        * @covers IP::parseCIDR
+        */
+       public function testCIDRParsing() {
+               $this->assertFalseCIDR( '192.0.2.0', "missing mask" );
+               $this->assertFalseCIDR( '192.0.2.0/', "missing bitmask" );
+
+               // Verify if statement
+               $this->assertFalseCIDR( '256.0.0.0/32', "invalid net" );
+               $this->assertFalseCIDR( '192.0.2.0/AA', "mask not numeric" );
+               $this->assertFalseCIDR( '192.0.2.0/-1', "mask < 0" );
+               $this->assertFalseCIDR( '192.0.2.0/33', "mask > 32" );
+
+               // Check internal logic
+               # 0 mask always result in array(0,0)
+               $this->assertEquals( [ 0, 0 ], IP::parseCIDR( '192.0.0.2/0' ) );
+               $this->assertEquals( [ 0, 0 ], IP::parseCIDR( '0.0.0.0/0' ) );
+               $this->assertEquals( [ 0, 0 ], IP::parseCIDR( '255.255.255.255/0' ) );
+
+               // @todo FIXME: Add more tests.
+
+               # This part test network shifting
+               $this->assertNet( '192.0.0.0', '192.0.0.2/24' );
+               $this->assertNet( '192.168.5.0', '192.168.5.13/24' );
+               $this->assertNet( '10.0.0.160', '10.0.0.161/28' );
+               $this->assertNet( '10.0.0.0', '10.0.0.3/28' );
+               $this->assertNet( '10.0.0.0', '10.0.0.3/30' );
+               $this->assertNet( '10.0.0.4', '10.0.0.4/30' );
+               $this->assertNet( '172.17.32.0', '172.17.35.48/21' );
+               $this->assertNet( '10.128.0.0', '10.135.0.0/9' );
+               $this->assertNet( '134.0.0.0', '134.0.5.1/8' );
+       }
+
+       /**
+        * @covers IP::canonicalize
+        */
+       public function testIPCanonicalizeOnValidIp() {
+               $this->assertEquals( '192.0.2.152', IP::canonicalize( '192.0.2.152' ),
+                       'Canonicalization of a valid IP returns it unchanged' );
+       }
+
+       /**
+        * @covers IP::canonicalize
+        */
+       public function testIPCanonicalizeMappedAddress() {
+               $this->assertEquals(
+                       '192.0.2.152',
+                       IP::canonicalize( '::ffff:192.0.2.152' )
+               );
+               $this->assertEquals(
+                       '192.0.2.152',
+                       IP::canonicalize( '::192.0.2.152' )
+               );
+       }
+
+       /**
+        * Issues there are most probably from IP::toHex() or IP::parseRange()
+        * @covers IP::isInRange
+        * @dataProvider provideIPsAndRanges
+        */
+       public function testIPIsInRange( $expected, $addr, $range, $message = '' ) {
+               $this->assertEquals(
+                       $expected,
+                       IP::isInRange( $addr, $range ),
+                       $message
+               );
+       }
+
+       /** Provider for testIPIsInRange() */
+       public static function provideIPsAndRanges() {
+               # Format: (expected boolean, address, range, optional message)
+               return [
+                       # IPv4
+                       [ true, '192.0.2.0', '192.0.2.0/24', 'Network address' ],
+                       [ true, '192.0.2.77', '192.0.2.0/24', 'Simple address' ],
+                       [ true, '192.0.2.255', '192.0.2.0/24', 'Broadcast address' ],
+
+                       [ false, '0.0.0.0', '192.0.2.0/24' ],
+                       [ false, '255.255.255', '192.0.2.0/24' ],
+
+                       # IPv6
+                       [ false, '::1', '2001:DB8::/32' ],
+                       [ false, '::', '2001:DB8::/32' ],
+                       [ false, 'FE80::1', '2001:DB8::/32' ],
+
+                       [ true, '2001:DB8::', '2001:DB8::/32' ],
+                       [ true, '2001:0DB8::', '2001:DB8::/32' ],
+                       [ true, '2001:DB8::1', '2001:DB8::/32' ],
+                       [ true, '2001:0DB8::1', '2001:DB8::/32' ],
+                       [ true, '2001:0DB8:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF',
+                               '2001:DB8::/32' ],
+
+                       [ false, '2001:0DB8:F::', '2001:DB8::/96' ],
+               ];
+       }
+
+       /**
+        * @covers IP::splitHostAndPort()
+        * @dataProvider provideSplitHostAndPort
+        */
+       public function testSplitHostAndPort( $expected, $input, $description ) {
+               $this->assertEquals( $expected, IP::splitHostAndPort( $input ), $description );
+       }
+
+       /**
+        * Provider for IP::splitHostAndPort()
+        */
+       public static function provideSplitHostAndPort() {
+               return [
+                       [ false, '[', 'Unclosed square bracket' ],
+                       [ false, '[::', 'Unclosed square bracket 2' ],
+                       [ [ '::', false ], '::', 'Bare IPv6 0' ],
+                       [ [ '::1', false ], '::1', 'Bare IPv6 1' ],
+                       [ [ '::', false ], '[::]', 'Bracketed IPv6 0' ],
+                       [ [ '::1', false ], '[::1]', 'Bracketed IPv6 1' ],
+                       [ [ '::1', 80 ], '[::1]:80', 'Bracketed IPv6 with port' ],
+                       [ false, '::x', 'Double colon but no IPv6' ],
+                       [ [ 'x', 80 ], 'x:80', 'Hostname and port' ],
+                       [ false, 'x:x', 'Hostname and invalid port' ],
+                       [ [ 'x', false ], 'x', 'Plain hostname' ]
+               ];
+       }
+
+       /**
+        * @covers IP::combineHostAndPort()
+        * @dataProvider provideCombineHostAndPort
+        */
+       public function testCombineHostAndPort( $expected, $input, $description ) {
+               list( $host, $port, $defaultPort ) = $input;
+               $this->assertEquals(
+                       $expected,
+                       IP::combineHostAndPort( $host, $port, $defaultPort ),
+                       $description );
+       }
+
+       /**
+        * Provider for IP::combineHostAndPort()
+        */
+       public static function provideCombineHostAndPort() {
+               return [
+                       [ '[::1]', [ '::1', 2, 2 ], 'IPv6 default port' ],
+                       [ '[::1]:2', [ '::1', 2, 3 ], 'IPv6 non-default port' ],
+                       [ 'x', [ 'x', 2, 2 ], 'Normal default port' ],
+                       [ 'x:2', [ 'x', 2, 3 ], 'Normal non-default port' ],
+               ];
+       }
+
+       /**
+        * @covers IP::sanitizeRange()
+        * @dataProvider provideIPCIDRs
+        */
+       public function testSanitizeRange( $input, $expected, $description ) {
+               $this->assertEquals( $expected, IP::sanitizeRange( $input ), $description );
+       }
+
+       /**
+        * Provider for IP::testSanitizeRange()
+        */
+       public static function provideIPCIDRs() {
+               return [
+                       [ '35.56.31.252/16', '35.56.0.0/16', 'IPv4 range' ],
+                       [ '135.16.21.252/24', '135.16.21.0/24', 'IPv4 range' ],
+                       [ '5.36.71.252/32', '5.36.71.252/32', 'IPv4 silly range' ],
+                       [ '5.36.71.252', '5.36.71.252', 'IPv4 non-range' ],
+                       [ '0:1:2:3:4:c5:f6:7/96', '0:1:2:3:4:C5:0:0/96', 'IPv6 range' ],
+                       [ '0:1:2:3:4:5:6:7/120', '0:1:2:3:4:5:6:0/120', 'IPv6 range' ],
+                       [ '0:e1:2:3:4:5:e6:7/128', '0:E1:2:3:4:5:E6:7/128', 'IPv6 silly range' ],
+                       [ '0:c1:A2:3:4:5:c6:7', '0:C1:A2:3:4:5:C6:7', 'IPv6 non range' ],
+               ];
+       }
+
+       /**
+        * @covers IP::prettifyIP()
+        * @dataProvider provideIPsToPrettify
+        */
+       public function testPrettifyIP( $ip, $prettified ) {
+               $this->assertEquals( $prettified, IP::prettifyIP( $ip ), "Prettify of $ip" );
+       }
+
+       /**
+        * Provider for IP::testPrettifyIP()
+        */
+       public static function provideIPsToPrettify() {
+               return [
+                       [ '0:0:0:0:0:0:0:0', '::' ],
+                       [ '0:0:0::0:0:0', '::' ],
+                       [ '0:0:0:1:0:0:0:0', '0:0:0:1::' ],
+                       [ '0:0::f', '::f' ],
+                       [ '0::0:0:0:33:fef:b', '::33:fef:b' ],
+                       [ '3f:535:0:0:0:0:e:fbb', '3f:535::e:fbb' ],
+                       [ '0:0:fef:0:0:0:e:fbb', '0:0:fef::e:fbb' ],
+                       [ 'abbc:2004::0:0:0:0', 'abbc:2004::' ],
+                       [ 'cebc:2004:f:0:0:0:0:0', 'cebc:2004:f::' ],
+                       [ '0:0:0:0:0:0:0:0/16', '::/16' ],
+                       [ '0:0:0::0:0:0/64', '::/64' ],
+                       [ '0:0::f/52', '::f/52' ],
+                       [ '::0:0:33:fef:b/52', '::33:fef:b/52' ],
+                       [ '3f:535:0:0:0:0:e:fbb/48', '3f:535::e:fbb/48' ],
+                       [ '0:0:fef:0:0:0:e:fbb/96', '0:0:fef::e:fbb/96' ],
+                       [ 'abbc:2004:0:0::0:0/40', 'abbc:2004::/40' ],
+                       [ 'aebc:2004:f:0:0:0:0:0/80', 'aebc:2004:f::/80' ],
+               ];
+       }
+}
diff --git a/tests/phpunit/unit/includes/libs/JavaScriptMinifierTest.php b/tests/phpunit/unit/includes/libs/JavaScriptMinifierTest.php
new file mode 100644 (file)
index 0000000..d57d0dd
--- /dev/null
@@ -0,0 +1,367 @@
+<?php
+
+class JavaScriptMinifierTest extends PHPUnit\Framework\TestCase {
+
+       use MediaWikiCoversValidator;
+
+       protected function tearDown() {
+               parent::tearDown();
+               // Reset
+               $this->setMaxLineLength( 1000 );
+       }
+
+       private function setMaxLineLength( $val ) {
+               $classReflect = new ReflectionClass( JavaScriptMinifier::class );
+               $propertyReflect = $classReflect->getProperty( 'maxLineLength' );
+               $propertyReflect->setAccessible( true );
+               $propertyReflect->setValue( JavaScriptMinifier::class, $val );
+       }
+
+       public static function provideCases() {
+               return [
+
+                       // Basic whitespace and comments that should be stripped entirely
+                       [ "\r\t\f \v\n\r", "" ],
+                       [ "/* Foo *\n*bar\n*/", "" ],
+
+                       /**
+                        * Slashes used inside block comments (T28931).
+                        * At some point there was a bug that caused this comment to be ended at '* /',
+                        * causing /M... to be left as the beginning of a regex.
+                        */
+                       [
+                               "/**\n * Foo\n * {\n * 'bar' : {\n * "
+                                       . "//Multiple rules with configurable operators\n * 'baz' : false\n * }\n */",
+                               "" ],
+
+                       /**
+                        * '  Foo \' bar \
+                        *  baz \' quox '  .
+                        */
+                       [
+                               "'  Foo  \\'  bar  \\\n  baz  \\'  quox  '  .length",
+                               "'  Foo  \\'  bar  \\\n  baz  \\'  quox  '.length"
+                       ],
+                       [
+                               "\"  Foo  \\\"  bar  \\\n  baz  \\\"  quox  \"  .length",
+                               "\"  Foo  \\\"  bar  \\\n  baz  \\\"  quox  \".length"
+                       ],
+                       [ "// Foo b/ar baz", "" ],
+                       [
+                               "/  Foo  \\/  bar  [  /  \\]  /  ]  baz  /  .length",
+                               "/  Foo  \\/  bar  [  /  \\]  /  ]  baz  /.length"
+                       ],
+
+                       // HTML comments
+                       [ "<!-- Foo bar", "" ],
+                       [ "<!-- Foo --> bar", "" ],
+                       [ "--> Foo", "" ],
+                       [ "x --> y", "x-->y" ],
+
+                       // Semicolon insertion
+                       [ "(function(){return\nx;})", "(function(){return\nx;})" ],
+                       [ "throw\nx;", "throw\nx;" ],
+                       [ "while(p){continue\nx;}", "while(p){continue\nx;}" ],
+                       [ "while(p){break\nx;}", "while(p){break\nx;}" ],
+                       [ "var\nx;", "var x;" ],
+                       [ "x\ny;", "x\ny;" ],
+                       [ "x\n++y;", "x\n++y;" ],
+                       [ "x\n!y;", "x\n!y;" ],
+                       [ "x\n{y}", "x\n{y}" ],
+                       [ "x\n+y;", "x+y;" ],
+                       [ "x\n(y);", "x(y);" ],
+                       [ "5.\nx;", "5.\nx;" ],
+                       [ "0xFF.\nx;", "0xFF.x;" ],
+                       [ "5.3.\nx;", "5.3.x;" ],
+
+                       // Cover failure case for incomplete hex literal
+                       [ "0x;", false, false ],
+
+                       // Cover failure case for number with no digits after E
+                       [ "1.4E", false, false ],
+
+                       // Cover failure case for number with several E
+                       [ "1.4EE2", false, false ],
+                       [ "1.4EE", false, false ],
+
+                       // Cover failure case for number with several E (nonconsecutive)
+                       // FIXME: This is invalid, but currently tolerated
+                       [ "1.4E2E3", "1.4E2 E3", false ],
+
+                       // Semicolon insertion between an expression having an inline
+                       // comment after it, and a statement on the next line (T29046).
+                       [
+                               "var a = this //foo bar \n for ( b = 0; c < d; b++ ) {}",
+                               "var a=this\nfor(b=0;c<d;b++){}"
+                       ],
+
+                       // Cover failure case of incomplete regexp at end of file (T75556)
+                       // FIXME: This is invalid, but currently tolerated
+                       [ "*/", "*/", false ],
+
+                       // Cover failure case of incomplete char class in regexp (T75556)
+                       // FIXME: This is invalid, but currently tolerated
+                       [ "/a[b/.test", "/a[b/.test", false ],
+
+                       // Cover failure case of incomplete string at end of file (T75556)
+                       // FIXME: This is invalid, but currently tolerated
+                       [ "'a", "'a", false ],
+
+                       // Token separation
+                       [ "x  in  y", "x in y" ],
+                       [ "/x/g  in  y", "/x/g in y" ],
+                       [ "x  in  30", "x in 30" ],
+                       [ "x  +  ++  y", "x+ ++y" ],
+                       [ "x ++  +  y", "x++ +y" ],
+                       [ "x  /  /y/.exec(z)", "x/ /y/.exec(z)" ],
+
+                       // State machine
+                       [ "/  x/g", "/  x/g" ],
+                       [ "(function(){return/  x/g})", "(function(){return/  x/g})" ],
+                       [ "+/  x/g", "+/  x/g" ],
+                       [ "++/  x/g", "++/  x/g" ],
+                       [ "x/  x/g", "x/x/g" ],
+                       [ "(/  x/g)", "(/  x/g)" ],
+                       [ "if(/  x/g);", "if(/  x/g);" ],
+                       [ "(x/  x/g)", "(x/x/g)" ],
+                       [ "([/  x/g])", "([/  x/g])" ],
+                       [ "+x/  x/g", "+x/x/g" ],
+                       [ "{}/  x/g", "{}/  x/g" ],
+                       [ "+{}/  x/g", "+{}/x/g" ],
+                       [ "(x)/  x/g", "(x)/x/g" ],
+                       [ "if(x)/  x/g", "if(x)/  x/g" ],
+                       [ "for(x;x;{}/  x/g);", "for(x;x;{}/x/g);" ],
+                       [ "x;x;{}/  x/g", "x;x;{}/  x/g" ],
+                       [ "x:{}/  x/g", "x:{}/  x/g" ],
+                       [ "switch(x){case y?z:{}/  x/g:{}/  x/g;}", "switch(x){case y?z:{}/x/g:{}/  x/g;}" ],
+                       [ "function x(){}/  x/g", "function x(){}/  x/g" ],
+                       [ "+function x(){}/  x/g", "+function x(){}/x/g" ],
+
+                       // Multiline quoted string
+                       [ "var foo=\"\\\nblah\\\n\";", "var foo=\"\\\nblah\\\n\";" ],
+
+                       // Multiline quoted string followed by string with spaces
+                       [
+                               "var foo=\"\\\nblah\\\n\";\nvar baz = \" foo \";\n",
+                               "var foo=\"\\\nblah\\\n\";var baz=\" foo \";"
+                       ],
+
+                       // URL in quoted string ( // is not a comment)
+                       [
+                               "aNode.setAttribute('href','http://foo.bar.org/baz');",
+                               "aNode.setAttribute('href','http://foo.bar.org/baz');"
+                       ],
+
+                       // URL in quoted string after multiline quoted string
+                       [
+                               "var foo=\"\\\nblah\\\n\";\naNode.setAttribute('href','http://foo.bar.org/baz');",
+                               "var foo=\"\\\nblah\\\n\";aNode.setAttribute('href','http://foo.bar.org/baz');"
+                       ],
+
+                       // Division vs. regex nastiness
+                       [
+                               "alert( (10+10) / '/'.charCodeAt( 0 ) + '//' );",
+                               "alert((10+10)/'/'.charCodeAt(0)+'//');"
+                       ],
+                       [ "if(1)/a /g.exec('Pa ss');", "if(1)/a /g.exec('Pa ss');" ],
+
+                       // Unicode letter characters should pass through ok in identifiers (T33187)
+                       [ "var KaŝSkatolVal = {}", 'var KaŝSkatolVal={}' ],
+
+                       // Per spec unicode char escape values should work in identifiers,
+                       // as long as it's a valid char. In future it might get normalized.
+                       [ "var Ka\\u015dSkatolVal = {}", 'var Ka\\u015dSkatolVal={}' ],
+
+                       // Some structures that might look invalid at first sight
+                       [ "var a = 5.;", "var a=5.;" ],
+                       [ "5.0.toString();", "5.0.toString();" ],
+                       [ "5..toString();", "5..toString();" ],
+                       // Cover failure case for too many decimal points
+                       [ "5...toString();", false ],
+                       [ "5.\n.toString();", '5..toString();' ],
+
+                       // Boolean minification (!0 / !1)
+                       [ "var a = { b: true };", "var a={b:!0};" ],
+                       [ "var a = { true: 12 };", "var a={true:12};" ],
+                       [ "a.true = 12;", "a.true=12;" ],
+                       [ "a.foo = true;", "a.foo=!0;" ],
+                       [ "a.foo = false;", "a.foo=!1;" ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideCases
+        * @covers JavaScriptMinifier::minify
+        * @covers JavaScriptMinifier::parseError
+        */
+       public function testMinifyOutput( $code, $expectedOutput, $expectedValid = true ) {
+               $minified = JavaScriptMinifier::minify( $code );
+
+               // JSMin+'s parser will throw an exception if output is not valid JS.
+               // suppression of warnings needed for stupid crap
+               if ( $expectedValid ) {
+                       Wikimedia\suppressWarnings();
+                       $parser = new JSParser();
+                       Wikimedia\restoreWarnings();
+                       $parser->parse( $minified, 'minify-test.js', 1 );
+               }
+
+               $this->assertEquals(
+                       $expectedOutput,
+                       $minified,
+                       "Minified output should be in the form expected."
+               );
+       }
+
+       public static function provideLineBreaker() {
+               return [
+                       [
+                               // Regression tests for T34548.
+                               // Must not break between 'E' and '+'.
+                               'var name = 1.23456789E55;',
+                               [
+                                       'var',
+                                       'name',
+                                       '=',
+                                       '1.23456789E55',
+                                       ';',
+                               ],
+                       ],
+                       [
+                               'var name = 1.23456789E+5;',
+                               [
+                                       'var',
+                                       'name',
+                                       '=',
+                                       '1.23456789E+5',
+                                       ';',
+                               ],
+                       ],
+                       [
+                               'var name = 1.23456789E-5;',
+                               [
+                                       'var',
+                                       'name',
+                                       '=',
+                                       '1.23456789E-5',
+                                       ';',
+                               ],
+                       ],
+                       [
+                               // Must not break before '++'
+                               'if(x++);',
+                               [
+                                       'if',
+                                       '(',
+                                       'x++',
+                                       ')',
+                                       ';',
+                               ],
+                       ],
+                       [
+                               // Regression test for T201606.
+                               // Must not break between 'return' and Expression.
+                               // Was caused by bad state after '{}' in property value.
+                               <<<JAVASCRIPT
+                       call( function () {
+                               try {
+                               } catch (e) {
+                                       obj = {
+                                               key: 1 ? 0 : {}
+                                       };
+                               }
+                               return name === 'input';
+                       } );
+JAVASCRIPT
+                               ,
+                               [
+                                       'call',
+                                       '(',
+                                       'function',
+                                       '(',
+                                       ')',
+                                       '{',
+                                       'try',
+                                       '{',
+                                       '}',
+                                       'catch',
+                                       '(',
+                                       'e',
+                                       ')',
+                                       '{',
+                                       'obj',
+                                       '=',
+                                       '{',
+                                       'key',
+                                       ':',
+                                       '1',
+                                       '?',
+                                       '0',
+                                       ':',
+                                       '{',
+                                       '}',
+                                       '}',
+                                       ';',
+                                       '}',
+                                       // The return Statement:
+                                       //     return [no LineTerminator here] Expression
+                                       'return name',
+                                       '===',
+                                       "'input'",
+                                       ';',
+                                       '}',
+                                       ')',
+                                       ';',
+                               ]
+                       ],
+                       [
+                               // Regression test for T201606.
+                               // Must not break between 'return' and Expression.
+                               // Was caused by bad state after a ternary in the expression value
+                               // for a key in an object literal.
+                               <<<JAVASCRIPT
+call( {
+       key: 1 ? 0 : function () {
+               return this;
+       }
+} );
+JAVASCRIPT
+                               ,
+                               [
+                                       'call',
+                                       '(',
+                                       '{',
+                                       'key',
+                                       ':',
+                                       '1',
+                                       '?',
+                                       '0',
+                                       ':',
+                                       'function',
+                                       '(',
+                                       ')',
+                                       '{',
+                                       'return this',
+                                       ';',
+                                       '}',
+                                       '}',
+                                       ')',
+                                       ';',
+                               ]
+                       ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideLineBreaker
+        * @covers JavaScriptMinifier::minify
+        */
+       public function testLineBreaker( $code, array $expectedLines ) {
+               $this->setMaxLineLength( 1 );
+               $actual = JavaScriptMinifier::minify( $code );
+               $this->assertEquals(
+                       array_merge( [ '' ], $expectedLines ),
+                       explode( "\n", $actual )
+               );
+       }
+}
diff --git a/tests/phpunit/unit/includes/libs/MapCacheLRUTest.php b/tests/phpunit/unit/includes/libs/MapCacheLRUTest.php
new file mode 100644 (file)
index 0000000..7147c6f
--- /dev/null
@@ -0,0 +1,267 @@
+<?php
+/**
+ * @group Cache
+ */
+class MapCacheLRUTest extends PHPUnit\Framework\TestCase {
+
+       use MediaWikiCoversValidator;
+
+       /**
+        * @covers MapCacheLRU::newFromArray()
+        * @covers MapCacheLRU::toArray()
+        * @covers MapCacheLRU::getAllKeys()
+        * @covers MapCacheLRU::clear()
+        * @covers MapCacheLRU::getMaxSize()
+        * @covers MapCacheLRU::setMaxSize()
+        */
+       function testArrayConversion() {
+               $raw = [ 'd' => 4, 'c' => 3, 'b' => 2, 'a' => 1 ];
+               $cache = MapCacheLRU::newFromArray( $raw, 3 );
+
+               $this->assertEquals( 3, $cache->getMaxSize() );
+               $this->assertSame( true, $cache->has( 'a' ) );
+               $this->assertSame( true, $cache->has( 'b' ) );
+               $this->assertSame( true, $cache->has( 'c' ) );
+               $this->assertSame( 1, $cache->get( 'a' ) );
+               $this->assertSame( 2, $cache->get( 'b' ) );
+               $this->assertSame( 3, $cache->get( 'c' ) );
+
+               $this->assertSame(
+                       [ 'a' => 1, 'b' => 2, 'c' => 3 ],
+                       $cache->toArray()
+               );
+               $this->assertSame(
+                       [ 'a', 'b', 'c' ],
+                       $cache->getAllKeys()
+               );
+
+               $cache->clear( 'a' );
+               $this->assertSame(
+                       [ 'b' => 2, 'c' => 3 ],
+                       $cache->toArray()
+               );
+
+               $cache->clear();
+               $this->assertSame(
+                       [],
+                       $cache->toArray()
+               );
+
+               $cache = MapCacheLRU::newFromArray( [ 'd' => 4, 'c' => 3, 'b' => 2, 'a' => 1 ], 4 );
+               $cache->setMaxSize( 3 );
+               $this->assertSame(
+                       [ 'c' => 3, 'b' => 2, 'a' => 1 ],
+                       $cache->toArray()
+               );
+       }
+
+       /**
+        * @covers MapCacheLRU::serialize()
+        * @covers MapCacheLRU::unserialize()
+        */
+       function testSerialize() {
+               $cache = MapCacheLRU::newFromArray( [ 'd' => 4, 'c' => 3, 'b' => 2, 'a' => 1 ], 10 );
+               $string = serialize( $cache );
+               $ncache = unserialize( $string );
+               $this->assertSame(
+                       [ 'd' => 4, 'c' => 3, 'b' => 2, 'a' => 1 ],
+                       $ncache->toArray()
+               );
+       }
+
+       /**
+        * @covers MapCacheLRU::has()
+        * @covers MapCacheLRU::get()
+        * @covers MapCacheLRU::set()
+        */
+       function testLRU() {
+               $raw = [ 'a' => 1, 'b' => 2, 'c' => 3 ];
+               $cache = MapCacheLRU::newFromArray( $raw, 3 );
+
+               $this->assertSame( true, $cache->has( 'c' ) );
+               $this->assertSame(
+                       [ 'a' => 1, 'b' => 2, 'c' => 3 ],
+                       $cache->toArray()
+               );
+
+               $this->assertSame( 3, $cache->get( 'c' ) );
+               $this->assertSame(
+                       [ 'a' => 1, 'b' => 2, 'c' => 3 ],
+                       $cache->toArray()
+               );
+
+               $this->assertSame( 1, $cache->get( 'a' ) );
+               $this->assertSame(
+                       [ 'b' => 2, 'c' => 3, 'a' => 1 ],
+                       $cache->toArray()
+               );
+
+               $cache->set( 'a', 1 );
+               $this->assertSame(
+                       [ 'b' => 2, 'c' => 3, 'a' => 1 ],
+                       $cache->toArray()
+               );
+
+               $cache->set( 'b', 22 );
+               $this->assertSame(
+                       [ 'c' => 3, 'a' => 1, 'b' => 22 ],
+                       $cache->toArray()
+               );
+
+               $cache->set( 'd', 4 );
+               $this->assertSame(
+                       [ 'a' => 1, 'b' => 22, 'd' => 4 ],
+                       $cache->toArray()
+               );
+
+               $cache->set( 'e', 5, 0.33 );
+               $this->assertSame(
+                       [ 'e' => 5, 'b' => 22, 'd' => 4 ],
+                       $cache->toArray()
+               );
+
+               $cache->set( 'f', 6, 0.66 );
+               $this->assertSame(
+                       [ 'b' => 22, 'f' => 6, 'd' => 4 ],
+                       $cache->toArray()
+               );
+
+               $cache->set( 'g', 7, 0.90 );
+               $this->assertSame(
+                       [ 'f' => 6, 'g' => 7, 'd' => 4 ],
+                       $cache->toArray()
+               );
+
+               $cache->set( 'g', 7, 1.0 );
+               $this->assertSame(
+                       [ 'f' => 6, 'd' => 4, 'g' => 7 ],
+                       $cache->toArray()
+               );
+       }
+
+       /**
+        * @covers MapCacheLRU::has()
+        * @covers MapCacheLRU::get()
+        * @covers MapCacheLRU::set()
+        */
+       public function testExpiry() {
+               $raw = [ 'a' => 1, 'b' => 2, 'c' => 3 ];
+               $cache = MapCacheLRU::newFromArray( $raw, 3 );
+
+               $now = microtime( true );
+               $cache->setMockTime( $now );
+
+               $cache->set( 'd', 'xxx' );
+               $this->assertTrue( $cache->has( 'd', 30 ) );
+               $this->assertEquals( 'xxx', $cache->get( 'd' ) );
+
+               $now += 29;
+               $this->assertTrue( $cache->has( 'd', 30 ) );
+               $this->assertEquals( 'xxx', $cache->get( 'd' ) );
+               $this->assertEquals( 'xxx', $cache->get( 'd', 30 ) );
+
+               $now += 1.5;
+               $this->assertFalse( $cache->has( 'd', 30 ) );
+               $this->assertEquals( 'xxx', $cache->get( 'd' ) );
+               $this->assertNull( $cache->get( 'd', 30 ) );
+       }
+
+       /**
+        * @covers MapCacheLRU::hasField()
+        * @covers MapCacheLRU::getField()
+        * @covers MapCacheLRU::setField()
+        */
+       public function testFields() {
+               $raw = [ 'a' => 1, 'b' => 2, 'c' => 3 ];
+               $cache = MapCacheLRU::newFromArray( $raw, 3 );
+
+               $now = microtime( true );
+               $cache->setMockTime( $now );
+
+               $cache->setField( 'PMs', 'Tony Blair', 'Labour' );
+               $cache->setField( 'PMs', 'Margaret Thatcher', 'Tory' );
+               $this->assertTrue( $cache->hasField( 'PMs', 'Tony Blair', 30 ) );
+               $this->assertEquals( 'Labour', $cache->getField( 'PMs', 'Tony Blair' ) );
+               $this->assertTrue( $cache->hasField( 'PMs', 'Tony Blair', 30 ) );
+
+               $now += 29;
+               $this->assertTrue( $cache->hasField( 'PMs', 'Tony Blair', 30 ) );
+               $this->assertEquals( 'Labour', $cache->getField( 'PMs', 'Tony Blair' ) );
+               $this->assertEquals( 'Labour', $cache->getField( 'PMs', 'Tony Blair', 30 ) );
+
+               $now += 1.5;
+               $this->assertFalse( $cache->hasField( 'PMs', 'Tony Blair', 30 ) );
+               $this->assertEquals( 'Labour', $cache->getField( 'PMs', 'Tony Blair' ) );
+               $this->assertNull( $cache->getField( 'PMs', 'Tony Blair', 30 ) );
+
+               $this->assertEquals(
+                       [ 'Tony Blair' => 'Labour', 'Margaret Thatcher' => 'Tory' ],
+                       $cache->get( 'PMs' )
+               );
+
+               $cache->set( 'MPs', [
+                       'Edwina Currie' => 1983,
+                       'Neil Kinnock' => 1970
+               ] );
+               $this->assertEquals(
+                       [
+                               'Edwina Currie' => 1983,
+                               'Neil Kinnock' => 1970
+                       ],
+                       $cache->get( 'MPs' )
+               );
+
+               $this->assertEquals( 1983, $cache->getField( 'MPs', 'Edwina Currie' ) );
+               $this->assertEquals( 1970, $cache->getField( 'MPs', 'Neil Kinnock' ) );
+       }
+
+       /**
+        * @covers MapCacheLRU::has()
+        * @covers MapCacheLRU::get()
+        * @covers MapCacheLRU::set()
+        * @covers MapCacheLRU::hasField()
+        * @covers MapCacheLRU::getField()
+        * @covers MapCacheLRU::setField()
+        */
+       public function testInvalidKeys() {
+               $cache = MapCacheLRU::newFromArray( [], 3 );
+
+               try {
+                       $cache->has( 3.4 );
+                       $this->fail( "No exception" );
+               } catch ( UnexpectedValueException $e ) {
+                       $this->assertRegExp( '/must be string or integer/', $e->getMessage() );
+               }
+               try {
+                       $cache->get( false );
+                       $this->fail( "No exception" );
+               } catch ( UnexpectedValueException $e ) {
+                       $this->assertRegExp( '/must be string or integer/', $e->getMessage() );
+               }
+               try {
+                       $cache->set( 3.4, 'x' );
+                       $this->fail( "No exception" );
+               } catch ( UnexpectedValueException $e ) {
+                       $this->assertRegExp( '/must be string or integer/', $e->getMessage() );
+               }
+
+               try {
+                       $cache->hasField( 'x', 3.4 );
+                       $this->fail( "No exception" );
+               } catch ( UnexpectedValueException $e ) {
+                       $this->assertRegExp( '/must be string or integer/', $e->getMessage() );
+               }
+               try {
+                       $cache->getField( 'x', false );
+                       $this->fail( "No exception" );
+               } catch ( UnexpectedValueException $e ) {
+                       $this->assertRegExp( '/must be string or integer/', $e->getMessage() );
+               }
+               try {
+                       $cache->setField( 'x', 3.4, 'x' );
+                       $this->fail( "No exception" );
+               } catch ( UnexpectedValueException $e ) {
+                       $this->assertRegExp( '/must be string or integer/', $e->getMessage() );
+               }
+       }
+}
diff --git a/tests/phpunit/unit/includes/libs/MemoizedCallableTest.php b/tests/phpunit/unit/includes/libs/MemoizedCallableTest.php
new file mode 100644 (file)
index 0000000..628cca0
--- /dev/null
@@ -0,0 +1,142 @@
+<?php
+/**
+ * PHPUnit tests for MemoizedCallable class.
+ * @covers MemoizedCallable
+ */
+class MemoizedCallableTest extends PHPUnit\Framework\TestCase {
+
+       use MediaWikiCoversValidator;
+
+       /**
+        * The memoized callable should relate inputs to outputs in the same
+        * way as the original underlying callable.
+        */
+       public function testReturnValuePassedThrough() {
+               $mock = $this->getMockBuilder( stdClass::class )
+                       ->setMethods( [ 'reverse' ] )->getMock();
+               $mock->expects( $this->any() )
+                       ->method( 'reverse' )
+                       ->will( $this->returnCallback( 'strrev' ) );
+
+               $memoized = new MemoizedCallable( [ $mock, 'reverse' ] );
+               $this->assertEquals( 'flow', $memoized->invoke( 'wolf' ) );
+       }
+
+       /**
+        * Consecutive calls to the memoized callable with the same arguments
+        * should result in just one invocation of the underlying callable.
+        *
+        * @requires extension apcu
+        */
+       public function testCallableMemoized() {
+               $observer = $this->getMockBuilder( stdClass::class )
+                       ->setMethods( [ 'computeSomething' ] )->getMock();
+               $observer->expects( $this->once() )
+                       ->method( 'computeSomething' )
+                       ->will( $this->returnValue( 'ok' ) );
+
+               $memoized = new ArrayBackedMemoizedCallable( [ $observer, 'computeSomething' ] );
+
+               // First invocation -- delegates to $observer->computeSomething()
+               $this->assertEquals( 'ok', $memoized->invoke() );
+
+               // Second invocation -- returns memoized result
+               $this->assertEquals( 'ok', $memoized->invoke() );
+       }
+
+       /**
+        * @covers MemoizedCallable::invoke
+        */
+       public function testInvokeVariadic() {
+               $memoized = new MemoizedCallable( 'sprintf' );
+               $this->assertEquals(
+                       $memoized->invokeArgs( [ 'this is %s', 'correct' ] ),
+                       $memoized->invoke( 'this is %s', 'correct' )
+               );
+       }
+
+       /**
+        * @covers MemoizedCallable::call
+        */
+       public function testShortcutMethod() {
+               $this->assertEquals(
+                       'this is correct',
+                       MemoizedCallable::call( 'sprintf', [ 'this is %s', 'correct' ] )
+               );
+       }
+
+       /**
+        * Outlier TTL values should be coerced to range 1 - 86400.
+        */
+       public function testTTLMaxMin() {
+               $memoized = new MemoizedCallable( 'abs', 100000 );
+               $this->assertEquals( 86400, $this->readAttribute( $memoized, 'ttl' ) );
+
+               $memoized = new MemoizedCallable( 'abs', -10 );
+               $this->assertEquals( 1, $this->readAttribute( $memoized, 'ttl' ) );
+       }
+
+       /**
+        * Closure names should be distinct.
+        */
+       public function testMemoizedClosure() {
+               $a = new MemoizedCallable( function () {
+                       return 'a';
+               } );
+
+               $b = new MemoizedCallable( function () {
+                       return 'b';
+               } );
+
+               $this->assertEquals( $a->invokeArgs(), 'a' );
+               $this->assertEquals( $b->invokeArgs(), 'b' );
+
+               $this->assertNotEquals(
+                       $this->readAttribute( $a, 'callableName' ),
+                       $this->readAttribute( $b, 'callableName' )
+               );
+
+               $c = new ArrayBackedMemoizedCallable( function () {
+                       return rand();
+               } );
+               $this->assertEquals( $c->invokeArgs(), $c->invokeArgs(), 'memoized random' );
+       }
+
+       /**
+        * @expectedExceptionMessage non-scalar argument
+        * @expectedException        InvalidArgumentException
+        */
+       public function testNonScalarArguments() {
+               $memoized = new MemoizedCallable( 'gettype' );
+               $memoized->invoke( new stdClass() );
+       }
+
+       /**
+        * @expectedExceptionMessage must be an instance of callable
+        * @expectedException        InvalidArgumentException
+        */
+       public function testNotCallable() {
+               $memoized = new MemoizedCallable( 14 );
+       }
+}
+
+/**
+ * A MemoizedCallable subclass that stores function return values
+ * in an instance property rather than APC or APCu.
+ */
+class ArrayBackedMemoizedCallable extends MemoizedCallable {
+       private $cache = [];
+
+       protected function fetchResult( $key, &$success ) {
+               if ( array_key_exists( $key, $this->cache ) ) {
+                       $success = true;
+                       return $this->cache[$key];
+               }
+               $success = false;
+               return false;
+       }
+
+       protected function storeResult( $key, $result ) {
+               $this->cache[$key] = $result;
+       }
+}
diff --git a/tests/phpunit/unit/includes/libs/ProcessCacheLRUTest.php b/tests/phpunit/unit/includes/libs/ProcessCacheLRUTest.php
new file mode 100644 (file)
index 0000000..8e91e70
--- /dev/null
@@ -0,0 +1,264 @@
+<?php
+
+/**
+ * Note that it uses the ProcessCacheLRUTestable class which extends some
+ * properties and methods visibility. That class is defined at the end of the
+ * file containing this class.
+ *
+ * @group Cache
+ */
+class ProcessCacheLRUTest extends PHPUnit\Framework\TestCase {
+
+       use MediaWikiCoversValidator;
+
+       /**
+        * Helper to verify emptiness of a cache object.
+        * Compare against an array so we get the cache content difference.
+        */
+       protected function assertCacheEmpty( $cache, $msg = 'Cache should be empty' ) {
+               $this->assertEquals( 0, $cache->getEntriesCount(), $msg );
+       }
+
+       /**
+        * Helper to fill a cache object passed by reference
+        */
+       protected function fillCache( &$cache, $numEntries ) {
+               // Fill cache with three values
+               for ( $i = 1; $i <= $numEntries; $i++ ) {
+                       $cache->set( "cache-key-$i", "prop-$i", "value-$i" );
+               }
+       }
+
+       /**
+        * Generates an array of what would be expected in cache for a given cache
+        * size and a number of entries filled in sequentially
+        */
+       protected function getExpectedCache( $cacheMaxEntries, $entryToFill ) {
+               $expected = [];
+
+               if ( $entryToFill === 0 ) {
+                       // The cache is empty!
+                       return [];
+               } elseif ( $entryToFill <= $cacheMaxEntries ) {
+                       // Cache is not fully filled
+                       $firstKey = 1;
+               } else {
+                       // Cache overflowed
+                       $firstKey = 1 + $entryToFill - $cacheMaxEntries;
+               }
+
+               $lastKey = $entryToFill;
+
+               for ( $i = $firstKey; $i <= $lastKey; $i++ ) {
+                       $expected["cache-key-$i"] = [ "prop-$i" => "value-$i" ];
+               }
+
+               return $expected;
+       }
+
+       /**
+        * Highlight diff between assertEquals and assertNotSame
+        * @coversNothing
+        */
+       public function testPhpUnitArrayEquality() {
+               $one = [ 'A' => 1, 'B' => 2 ];
+               $two = [ 'B' => 2, 'A' => 1 ];
+               // ==
+               $this->assertEquals( $one, $two );
+               // ===
+               $this->assertNotSame( $one, $two );
+       }
+
+       /**
+        * @dataProvider provideInvalidConstructorArg
+        * @expectedException Wikimedia\Assert\ParameterAssertionException
+        * @covers ProcessCacheLRU::__construct
+        */
+       public function testConstructorGivenInvalidValue( $maxSize ) {
+               new ProcessCacheLRUTestable( $maxSize );
+       }
+
+       /**
+        * Value which are forbidden by the constructor
+        */
+       public static function provideInvalidConstructorArg() {
+               return [
+                       [ null ],
+                       [ [] ],
+                       [ new stdClass() ],
+                       [ 0 ],
+                       [ '5' ],
+                       [ -1 ],
+               ];
+       }
+
+       /**
+        * @covers ProcessCacheLRU::get
+        * @covers ProcessCacheLRU::set
+        * @covers ProcessCacheLRU::has
+        */
+       public function testAddAndGetAKey() {
+               $oneCache = new ProcessCacheLRUTestable( 1 );
+               $this->assertCacheEmpty( $oneCache );
+
+               // First set just one value
+               $oneCache->set( 'cache-key', 'prop1', 'value1' );
+               $this->assertEquals( 1, $oneCache->getEntriesCount() );
+               $this->assertTrue( $oneCache->has( 'cache-key', 'prop1' ) );
+               $this->assertEquals( 'value1', $oneCache->get( 'cache-key', 'prop1' ) );
+       }
+
+       /**
+        * @covers ProcessCacheLRU::set
+        * @covers ProcessCacheLRU::get
+        */
+       public function testDeleteOldKey() {
+               $oneCache = new ProcessCacheLRUTestable( 1 );
+               $this->assertCacheEmpty( $oneCache );
+
+               $oneCache->set( 'cache-key', 'prop1', 'value1' );
+               $oneCache->set( 'cache-key', 'prop1', 'value2' );
+               $this->assertEquals( 'value2', $oneCache->get( 'cache-key', 'prop1' ) );
+       }
+
+       /**
+        * This test that we properly overflow when filling a cache with
+        * a sequence of always different cache-keys. Meant to verify we correclty
+        * delete the older key.
+        *
+        * @covers ProcessCacheLRU::set
+        * @dataProvider provideCacheFilling
+        * @param int $cacheMaxEntries Maximum entry the created cache will hold
+        * @param int $entryToFill Number of entries to insert in the created cache.
+        */
+       public function testFillingCache( $cacheMaxEntries, $entryToFill, $msg = '' ) {
+               $cache = new ProcessCacheLRUTestable( $cacheMaxEntries );
+               $this->fillCache( $cache, $entryToFill );
+
+               $this->assertSame(
+                       $this->getExpectedCache( $cacheMaxEntries, $entryToFill ),
+                       $cache->getCache(),
+                       "Filling a $cacheMaxEntries entries cache with $entryToFill entries"
+               );
+       }
+
+       /**
+        * Provider for testFillingCache
+        */
+       public static function provideCacheFilling() {
+               // ($cacheMaxEntries, $entryToFill, $msg='')
+               return [
+                       [ 1, 0 ],
+                       [ 1, 1 ],
+                       // overflow
+                       [ 1, 2 ],
+                       // overflow
+                       [ 5, 33 ],
+               ];
+       }
+
+       /**
+        * Create a cache with only one remaining entry then update
+        * the first inserted entry. Should bump it to the top.
+        *
+        * @covers ProcessCacheLRU::set
+        */
+       public function testReplaceExistingKeyShouldBumpEntryToTop() {
+               $maxEntries = 3;
+
+               $cache = new ProcessCacheLRUTestable( $maxEntries );
+               // Fill cache leaving just one remaining slot
+               $this->fillCache( $cache, $maxEntries - 1 );
+
+               // Set an existing cache key
+               $cache->set( "cache-key-1", "prop-1", "new-value-for-1" );
+
+               $this->assertSame(
+                       [
+                               'cache-key-2' => [ 'prop-2' => 'value-2' ],
+                               'cache-key-1' => [ 'prop-1' => 'new-value-for-1' ],
+                       ],
+                       $cache->getCache()
+               );
+       }
+
+       /**
+        * @covers ProcessCacheLRU::get
+        * @covers ProcessCacheLRU::set
+        * @covers ProcessCacheLRU::has
+        */
+       public function testRecentlyAccessedKeyStickIn() {
+               $cache = new ProcessCacheLRUTestable( 2 );
+               $cache->set( 'first', 'prop1', 'value1' );
+               $cache->set( 'second', 'prop2', 'value2' );
+
+               // Get first
+               $cache->get( 'first', 'prop1' );
+               // Cache a third value, should invalidate the least used one
+               $cache->set( 'third', 'prop3', 'value3' );
+
+               $this->assertFalse( $cache->has( 'second', 'prop2' ) );
+       }
+
+       /**
+        * This first create a full cache then update the value for the 2nd
+        * filled entry.
+        * Given a cache having 1,2,3 as key, updating 2 should bump 2 to
+        * the top of the queue with the new value: 1,3,2* (* = updated).
+        *
+        * @covers ProcessCacheLRU::set
+        * @covers ProcessCacheLRU::get
+        */
+       public function testReplaceExistingKeyInAFullCacheShouldBumpToTop() {
+               $maxEntries = 3;
+
+               $cache = new ProcessCacheLRUTestable( $maxEntries );
+               $this->fillCache( $cache, $maxEntries );
+
+               // Set an existing cache key
+               $cache->set( "cache-key-2", "prop-2", "new-value-for-2" );
+               $this->assertSame(
+                       [
+                               'cache-key-1' => [ 'prop-1' => 'value-1' ],
+                               'cache-key-3' => [ 'prop-3' => 'value-3' ],
+                               'cache-key-2' => [ 'prop-2' => 'new-value-for-2' ],
+                       ],
+                       $cache->getCache()
+               );
+               $this->assertEquals( 'new-value-for-2',
+                       $cache->get( 'cache-key-2', 'prop-2' )
+               );
+       }
+
+       /**
+        * @covers ProcessCacheLRU::set
+        */
+       public function testBumpExistingKeyToTop() {
+               $cache = new ProcessCacheLRUTestable( 3 );
+               $this->fillCache( $cache, 3 );
+
+               // Set the very first cache key to a new value
+               $cache->set( "cache-key-1", "prop-1", "new value for 1" );
+               $this->assertEquals(
+                       [
+                               'cache-key-2' => [ 'prop-2' => 'value-2' ],
+                               'cache-key-3' => [ 'prop-3' => 'value-3' ],
+                               'cache-key-1' => [ 'prop-1' => 'new value for 1' ],
+                       ],
+                       $cache->getCache()
+               );
+       }
+}
+
+/**
+ * Overrides some ProcessCacheLRU methods and properties accessibility.
+ */
+class ProcessCacheLRUTestable extends ProcessCacheLRU {
+       public function getCache() {
+               return $this->cache->toArray();
+       }
+
+       public function getEntriesCount() {
+               return count( $this->cache->toArray() );
+       }
+}
diff --git a/tests/phpunit/unit/includes/libs/SamplingStatsdClientTest.php b/tests/phpunit/unit/includes/libs/SamplingStatsdClientTest.php
new file mode 100644 (file)
index 0000000..7bd1611
--- /dev/null
@@ -0,0 +1,77 @@
+<?php
+
+use Liuggio\StatsdClient\Entity\StatsdData;
+use Liuggio\StatsdClient\Sender\SenderInterface;
+
+/**
+ * @covers SamplingStatsdClient
+ */
+class SamplingStatsdClientTest extends PHPUnit\Framework\TestCase {
+
+       use MediaWikiCoversValidator;
+
+       /**
+        * @dataProvider samplingDataProvider
+        */
+       public function testSampling( $data, $sampleRate, $seed, $expectWrite ) {
+               $sender = $this->getMockBuilder( SenderInterface::class )->getMock();
+               $sender->expects( $this->any() )->method( 'open' )->will( $this->returnValue( true ) );
+               if ( $expectWrite ) {
+                       $sender->expects( $this->once() )->method( 'write' )
+                               ->with( $this->anything(), $this->equalTo( $data ) );
+               } else {
+                       $sender->expects( $this->never() )->method( 'write' );
+               }
+               if ( defined( 'MT_RAND_PHP' ) ) {
+                       mt_srand( $seed, MT_RAND_PHP );
+               } else {
+                       mt_srand( $seed );
+               }
+               $client = new SamplingStatsdClient( $sender );
+               $client->send( $data, $sampleRate );
+       }
+
+       public function samplingDataProvider() {
+               $unsampled = new StatsdData();
+               $unsampled->setKey( 'foo' );
+               $unsampled->setValue( 1 );
+
+               $sampled = new StatsdData();
+               $sampled->setKey( 'foo' );
+               $sampled->setValue( 1 );
+               $sampled->setSampleRate( '0.1' );
+
+               return [
+                       // $data, $sampleRate, $seed, $expectWrite
+                       [ $unsampled, 1, 0 /*0.44*/, true ],
+                       [ $sampled, 1, 0 /*0.44*/, false ],
+                       [ $sampled, 1, 4 /*0.03*/, true ],
+                       [ $unsampled, 0.1, 0 /*0.44*/, false ],
+                       [ $sampled, 0.5, 0 /*0.44*/, false ],
+                       [ $sampled, 0.5, 4 /*0.03*/, false ],
+               ];
+       }
+
+       public function testSetSamplingRates() {
+               $matching = new StatsdData();
+               $matching->setKey( 'foo.bar' );
+               $matching->setValue( 1 );
+
+               $nonMatching = new StatsdData();
+               $nonMatching->setKey( 'oof.bar' );
+               $nonMatching->setValue( 1 );
+
+               $sender = $this->getMockBuilder( SenderInterface::class )->getMock();
+               $sender->expects( $this->any() )->method( 'open' )->will( $this->returnValue( true ) );
+               $sender->expects( $this->once() )->method( 'write' )->with( $this->anything(),
+                       $this->equalTo( $nonMatching ) );
+
+               $client = new SamplingStatsdClient( $sender );
+               $client->setSamplingRates( [ 'foo.*' => 0.2 ] );
+
+               mt_srand( 0 ); // next random is 0.44
+               $client->send( $matching );
+               mt_srand( 0 );
+               $client->send( $nonMatching );
+       }
+}
diff --git a/tests/phpunit/unit/includes/libs/StaticArrayWriterTest.php b/tests/phpunit/unit/includes/libs/StaticArrayWriterTest.php
new file mode 100644 (file)
index 0000000..4bd845d
--- /dev/null
@@ -0,0 +1,58 @@
+<?php
+/**
+ * Copyright (C) 2018 Kunal Mehta <legoktm@member.fsf.org>
+ *
+ * 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; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * 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.
+ *
+ */
+
+use Wikimedia\StaticArrayWriter;
+
+/**
+ * @covers \Wikimedia\StaticArrayWriter
+ */
+class StaticArrayWriterTest extends PHPUnit\Framework\TestCase {
+       public function testCreate() {
+               $data = [
+                       'foo' => 'bar',
+                       'baz' => 'rawr',
+                       "they're" => '"quoted properly"',
+                       'nested' => [ 'elements', 'work' ],
+                       'and' => [ 'these' => 'do too' ],
+               ];
+               $writer = new StaticArrayWriter();
+               $actual = $writer->create( $data, "Header\nWith\nNewlines" );
+               $expected = <<<PHP
+<?php
+// Header
+// With
+// Newlines
+return [
+       'foo' => 'bar',
+       'baz' => 'rawr',
+       'they\'re' => '"quoted properly"',
+       'nested' => [
+               0 => 'elements',
+               1 => 'work',
+       ],
+       'and' => [
+               'these' => 'do too',
+       ],
+];
+
+PHP;
+               $this->assertSame( $expected, $actual );
+       }
+}
diff --git a/tests/phpunit/unit/includes/libs/StringUtilsTest.php b/tests/phpunit/unit/includes/libs/StringUtilsTest.php
new file mode 100644 (file)
index 0000000..fcfa53e
--- /dev/null
@@ -0,0 +1,128 @@
+<?php
+
+class StringUtilsTest extends PHPUnit\Framework\TestCase {
+
+       use MediaWikiCoversValidator;
+
+       /**
+        * @covers StringUtils::isUtf8
+        * @dataProvider provideStringsForIsUtf8Check
+        */
+       public function testIsUtf8( $expected, $string ) {
+               $this->assertEquals( $expected, StringUtils::isUtf8( $string ),
+                       'Testing string "' . $this->escaped( $string ) . '"' );
+       }
+
+       /**
+        * Print high range characters as a hexadecimal
+        * @param string $string
+        * @return string
+        */
+       function escaped( $string ) {
+               $escaped = '';
+               $length = strlen( $string );
+               for ( $i = 0; $i < $length; $i++ ) {
+                       $char = $string[$i];
+                       $val = ord( $char );
+                       if ( $val > 127 ) {
+                               $escaped .= '\x' . dechex( $val );
+                       } else {
+                               $escaped .= $char;
+                       }
+               }
+
+               return $escaped;
+       }
+
+       /**
+        * See also "UTF-8 decoder capability and stress test" by
+        * Markus Kuhn:
+        * http://www.cl.cam.ac.uk/~mgk25/ucs/examples/UTF-8-test.txt
+        */
+       public static function provideStringsForIsUtf8Check() {
+               // Expected return values for StringUtils::isUtf8()
+               $PASS = true;
+               $FAIL = false;
+
+               return [
+                       'some ASCII' => [ $PASS, 'Some ASCII' ],
+                       'euro sign' => [ $PASS, "Euro sign €" ],
+
+                       'first possible sequence 1 byte' => [ $PASS, "\x00" ],
+                       'first possible sequence 2 bytes' => [ $PASS, "\xc2\x80" ],
+                       'first possible sequence 3 bytes' => [ $PASS, "\xe0\xa0\x80" ],
+                       'first possible sequence 4 bytes' => [ $PASS, "\xf0\x90\x80\x80" ],
+                       'first possible sequence 5 bytes' => [ $FAIL, "\xf8\x88\x80\x80\x80" ],
+                       'first possible sequence 6 bytes' => [ $FAIL, "\xfc\x84\x80\x80\x80\x80" ],
+
+                       'last possible sequence 1 byte' => [ $PASS, "\x7f" ],
+                       'last possible sequence 2 bytes' => [ $PASS, "\xdf\xbf" ],
+                       'last possible sequence 3 bytes' => [ $PASS, "\xef\xbf\xbf" ],
+                       'last possible sequence 4 bytes (U+1FFFFF)' => [ $FAIL, "\xf7\xbf\xbf\xbf" ],
+                       'last possible sequence 5 bytes' => [ $FAIL, "\xfb\xbf\xbf\xbf\xbf" ],
+                       'last possible sequence 6 bytes' => [ $FAIL, "\xfd\xbf\xbf\xbf\xbf\xbf" ],
+
+                       'boundary 1' => [ $PASS, "\xed\x9f\xbf" ],
+                       'boundary 2' => [ $PASS, "\xee\x80\x80" ],
+                       'boundary 3' => [ $PASS, "\xef\xbf\xbd" ],
+                       'boundary 4' => [ $PASS, "\xf2\x80\x80\x80" ],
+                       'boundary 5 (U+FFFFF)' => [ $PASS, "\xf3\xbf\xbf\xbf" ],
+                       'boundary 6 (U+100000)' => [ $PASS, "\xf4\x80\x80\x80" ],
+                       'boundary 7 (U+10FFFF)' => [ $PASS, "\xf4\x8f\xbf\xbf" ],
+                       'boundary 8 (U+110000)' => [ $FAIL, "\xf4\x90\x80\x80" ],
+
+                       'malformed 1' => [ $FAIL, "\x80" ],
+                       'malformed 2' => [ $FAIL, "\xbf" ],
+                       'malformed 3' => [ $FAIL, "\x80\xbf" ],
+                       'malformed 4' => [ $FAIL, "\x80\xbf\x80" ],
+                       'malformed 5' => [ $FAIL, "\x80\xbf\x80\xbf" ],
+                       'malformed 6' => [ $FAIL, "\x80\xbf\x80\xbf\x80" ],
+                       'malformed 7' => [ $FAIL, "\x80\xbf\x80\xbf\x80\xbf" ],
+                       'malformed 8' => [ $FAIL, "\x80\xbf\x80\xbf\x80\xbf\x80" ],
+
+                       'last byte missing 1' => [ $FAIL, "\xc0" ],
+                       'last byte missing 2' => [ $FAIL, "\xe0\x80" ],
+                       'last byte missing 3' => [ $FAIL, "\xf0\x80\x80" ],
+                       'last byte missing 4' => [ $FAIL, "\xf8\x80\x80\x80" ],
+                       'last byte missing 5' => [ $FAIL, "\xfc\x80\x80\x80\x80" ],
+                       'last byte missing 6' => [ $FAIL, "\xdf" ],
+                       'last byte missing 7' => [ $FAIL, "\xef\xbf" ],
+                       'last byte missing 8' => [ $FAIL, "\xf7\xbf\xbf" ],
+                       'last byte missing 9' => [ $FAIL, "\xfb\xbf\xbf\xbf" ],
+                       'last byte missing 10' => [ $FAIL, "\xfd\xbf\xbf\xbf\xbf" ],
+
+                       'extra continuation byte 1' => [ $FAIL, "e\xaf" ],
+                       'extra continuation byte 2' => [ $FAIL, "\xc3\x89\xaf" ],
+                       'extra continuation byte 3' => [ $FAIL, "\xef\xbc\xa5\xaf" ],
+                       'extra continuation byte 4' => [ $FAIL, "\xf0\x9d\x99\xb4\xaf" ],
+
+                       'impossible bytes 1' => [ $FAIL, "\xfe" ],
+                       'impossible bytes 2' => [ $FAIL, "\xff" ],
+                       'impossible bytes 3' => [ $FAIL, "\xfe\xfe\xff\xff" ],
+
+                       'overlong sequences 1' => [ $FAIL, "\xc0\xaf" ],
+                       'overlong sequences 2' => [ $FAIL, "\xc1\xaf" ],
+                       'overlong sequences 3' => [ $FAIL, "\xe0\x80\xaf" ],
+                       'overlong sequences 4' => [ $FAIL, "\xf0\x80\x80\xaf" ],
+                       'overlong sequences 5' => [ $FAIL, "\xf8\x80\x80\x80\xaf" ],
+                       'overlong sequences 6' => [ $FAIL, "\xfc\x80\x80\x80\x80\xaf" ],
+
+                       'maximum overlong sequences 1' => [ $FAIL, "\xc1\xbf" ],
+                       'maximum overlong sequences 2' => [ $FAIL, "\xe0\x9f\xbf" ],
+                       'maximum overlong sequences 3' => [ $FAIL, "\xf0\x8f\xbf\xbf" ],
+                       'maximum overlong sequences 4' => [ $FAIL, "\xf8\x87\xbf\xbf" ],
+                       'maximum overlong sequences 5' => [ $FAIL, "\xfc\x83\xbf\xbf\xbf\xbf" ],
+
+                       'surrogates 1 (U+D799)' => [ $PASS, "\xed\x9f\xbf" ],
+                       'surrogates 2 (U+E000)' => [ $PASS, "\xee\x80\x80" ],
+                       'surrogates 3 (U+D800)' => [ $FAIL, "\xed\xa0\x80" ],
+                       'surrogates 4 (U+DBFF)' => [ $FAIL, "\xed\xaf\xbf" ],
+                       'surrogates 5 (U+DC00)' => [ $FAIL, "\xed\xb0\x80" ],
+                       'surrogates 6 (U+DFFF)' => [ $FAIL, "\xed\xbf\xbf" ],
+                       'surrogates 7 (U+D800 U+DC00)' => [ $FAIL, "\xed\xa0\x80\xed\xb0\x80" ],
+
+                       'noncharacters 1' => [ $PASS, "\xef\xbf\xbe" ],
+                       'noncharacters 2' => [ $PASS, "\xef\xbf\xbf" ],
+               ];
+       }
+}
diff --git a/tests/phpunit/unit/includes/libs/TimingTest.php b/tests/phpunit/unit/includes/libs/TimingTest.php
new file mode 100644 (file)
index 0000000..581a518
--- /dev/null
@@ -0,0 +1,115 @@
+<?php
+/**
+ * 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; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * 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.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @author Ori Livneh <ori@wikimedia.org>
+ */
+
+class TimingTest extends PHPUnit\Framework\TestCase {
+
+       use MediaWikiCoversValidator;
+
+       /**
+        * @covers Timing::clearMarks
+        * @covers Timing::getEntries
+        */
+       public function testClearMarks() {
+               $timing = new Timing;
+               $this->assertCount( 1, $timing->getEntries() );
+
+               $timing->mark( 'a' );
+               $timing->mark( 'b' );
+               $this->assertCount( 3, $timing->getEntries() );
+
+               $timing->clearMarks( 'a' );
+               $this->assertNull( $timing->getEntryByName( 'a' ) );
+               $this->assertNotNull( $timing->getEntryByName( 'b' ) );
+
+               $timing->clearMarks();
+               $this->assertCount( 1, $timing->getEntries() );
+       }
+
+       /**
+        * @covers Timing::mark
+        * @covers Timing::getEntryByName
+        */
+       public function testMark() {
+               $timing = new Timing;
+               $timing->mark( 'a' );
+
+               $entry = $timing->getEntryByName( 'a' );
+               $this->assertEquals( 'a', $entry['name'] );
+               $this->assertEquals( 'mark', $entry['entryType'] );
+               $this->assertArrayHasKey( 'startTime', $entry );
+               $this->assertEquals( 0, $entry['duration'] );
+
+               usleep( 100 );
+               $timing->mark( 'a' );
+               $newEntry = $timing->getEntryByName( 'a' );
+               $this->assertGreaterThan( $entry['startTime'], $newEntry['startTime'] );
+       }
+
+       /**
+        * @covers Timing::measure
+        */
+       public function testMeasure() {
+               $timing = new Timing;
+
+               $timing->mark( 'a' );
+               usleep( 100 );
+               $timing->mark( 'b' );
+
+               $a = $timing->getEntryByName( 'a' );
+               $b = $timing->getEntryByName( 'b' );
+
+               $timing->measure( 'a_to_b', 'a', 'b' );
+
+               $entry = $timing->getEntryByName( 'a_to_b' );
+               $this->assertEquals( 'a_to_b', $entry['name'] );
+               $this->assertEquals( 'measure', $entry['entryType'] );
+               $this->assertEquals( $a['startTime'], $entry['startTime'] );
+               $this->assertEquals( $b['startTime'] - $a['startTime'], $entry['duration'] );
+       }
+
+       /**
+        * @covers Timing::getEntriesByType
+        */
+       public function testGetEntriesByType() {
+               $timing = new Timing;
+
+               $timing->mark( 'mark_a' );
+               usleep( 100 );
+               $timing->mark( 'mark_b' );
+               usleep( 100 );
+               $timing->mark( 'mark_c' );
+
+               $timing->measure( 'measure_a', 'mark_a', 'mark_b' );
+               $timing->measure( 'measure_b', 'mark_b', 'mark_c' );
+
+               $marks = array_map( function ( $entry ) {
+                       return $entry['name'];
+               }, $timing->getEntriesByType( 'mark' ) );
+
+               $this->assertEquals( [ 'requestStart', 'mark_a', 'mark_b', 'mark_c' ], $marks );
+
+               $measures = array_map( function ( $entry ) {
+                       return $entry['name'];
+               }, $timing->getEntriesByType( 'measure' ) );
+
+               $this->assertEquals( [ 'measure_a', 'measure_b' ], $measures );
+       }
+}
diff --git a/tests/phpunit/unit/includes/libs/XhprofDataTest.php b/tests/phpunit/unit/includes/libs/XhprofDataTest.php
new file mode 100644 (file)
index 0000000..3e93794
--- /dev/null
@@ -0,0 +1,274 @@
+<?php
+/**
+ * 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; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * 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.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+/**
+ * @copyright © 2014 Wikimedia Foundation and contributors
+ * @since 1.25
+ */
+class XhprofDataTest extends PHPUnit\Framework\TestCase {
+
+       use MediaWikiCoversValidator;
+
+       /**
+        * @covers XhprofData::splitKey
+        * @dataProvider provideSplitKey
+        */
+       public function testSplitKey( $key, $expect ) {
+               $this->assertSame( $expect, XhprofData::splitKey( $key ) );
+       }
+
+       public function provideSplitKey() {
+               return [
+                       [ 'main()', [ null, 'main()' ] ],
+                       [ 'foo==>bar', [ 'foo', 'bar' ] ],
+                       [ 'bar@1==>bar@2', [ 'bar@1', 'bar@2' ] ],
+                       [ 'foo==>bar==>baz', [ 'foo', 'bar==>baz' ] ],
+                       [ '==>bar', [ '', 'bar' ] ],
+                       [ '', [ null, '' ] ],
+               ];
+       }
+
+       /**
+        * @covers XhprofData::pruneData
+        */
+       public function testInclude() {
+               $xhprofData = $this->getXhprofDataFixture( [
+                       'include' => [ 'main()' ],
+               ] );
+               $raw = $xhprofData->getRawData();
+               $this->assertArrayHasKey( 'main()', $raw );
+               $this->assertArrayHasKey( 'main()==>foo', $raw );
+               $this->assertArrayHasKey( 'main()==>xhprof_disable', $raw );
+               $this->assertSame( 3, count( $raw ) );
+       }
+
+       /**
+        * Validate the structure of data returned by
+        * Xhprof::getInclusiveMetrics(). This acts as a guard against unexpected
+        * structural changes to the returned data in lieu of using a more heavy
+        * weight typed response object.
+        *
+        * @covers XhprofData::getInclusiveMetrics
+        */
+       public function testInclusiveMetricsStructure() {
+               $metricStruct = [
+                       'ct' => 'int',
+                       'wt' => 'array',
+                       'cpu' => 'array',
+                       'mu' => 'array',
+                       'pmu' => 'array',
+               ];
+               $statStruct = [
+                       'total' => 'numeric',
+                       'min' => 'numeric',
+                       'mean' => 'numeric',
+                       'max' => 'numeric',
+                       'variance' => 'numeric',
+                       'percent' => 'numeric',
+               ];
+
+               $xhprofData = $this->getXhprofDataFixture();
+               $metrics = $xhprofData->getInclusiveMetrics();
+
+               foreach ( $metrics as $name => $metric ) {
+                       $this->assertArrayStructure( $metricStruct, $metric );
+
+                       foreach ( $metricStruct as $key => $type ) {
+                               if ( $type === 'array' ) {
+                                       $this->assertArrayStructure( $statStruct, $metric[$key] );
+                                       if ( $name === 'main()' ) {
+                                               $this->assertEquals( 100, $metric[$key]['percent'] );
+                                       }
+                               }
+                       }
+               }
+       }
+
+       /**
+        * Validate the structure of data returned by
+        * Xhprof::getCompleteMetrics(). This acts as a guard against unexpected
+        * structural changes to the returned data in lieu of using a more heavy
+        * weight typed response object.
+        *
+        * @covers XhprofData::getCompleteMetrics
+        */
+       public function testCompleteMetricsStructure() {
+               $metricStruct = [
+                       'ct' => 'int',
+                       'wt' => 'array',
+                       'cpu' => 'array',
+                       'mu' => 'array',
+                       'pmu' => 'array',
+                       'calls' => 'array',
+                       'subcalls' => 'array',
+               ];
+               $statsMetrics = [ 'wt', 'cpu', 'mu', 'pmu' ];
+               $statStruct = [
+                       'total' => 'numeric',
+                       'min' => 'numeric',
+                       'mean' => 'numeric',
+                       'max' => 'numeric',
+                       'variance' => 'numeric',
+                       'percent' => 'numeric',
+                       'exclusive' => 'numeric',
+               ];
+
+               $xhprofData = $this->getXhprofDataFixture();
+               $metrics = $xhprofData->getCompleteMetrics();
+
+               foreach ( $metrics as $name => $metric ) {
+                       $this->assertArrayStructure( $metricStruct, $metric, $name );
+
+                       foreach ( $metricStruct as $key => $type ) {
+                               if ( in_array( $key, $statsMetrics ) ) {
+                                       $this->assertArrayStructure(
+                                               $statStruct, $metric[$key], $key
+                                       );
+                                       $this->assertLessThanOrEqual(
+                                               $metric[$key]['total'], $metric[$key]['exclusive']
+                                       );
+                               }
+                       }
+               }
+       }
+
+       /**
+        * @covers XhprofData::getCallers
+        * @covers XhprofData::getCallees
+        */
+       public function testEdges() {
+               $xhprofData = $this->getXhprofDataFixture();
+               $this->assertSame( [], $xhprofData->getCallers( 'main()' ) );
+               $this->assertSame( [ 'foo', 'xhprof_disable' ],
+                       $xhprofData->getCallees( 'main()' )
+               );
+               $this->assertSame( [ 'main()' ],
+                       $xhprofData->getCallers( 'foo' )
+               );
+               $this->assertSame( [], $xhprofData->getCallees( 'strlen' ) );
+       }
+
+       /**
+        * @covers XhprofData::getCriticalPath
+        */
+       public function testCriticalPath() {
+               $xhprofData = $this->getXhprofDataFixture();
+               $path = $xhprofData->getCriticalPath();
+
+               $last = null;
+               foreach ( $path as $key => $value ) {
+                       list( $func, $call ) = XhprofData::splitKey( $key );
+                       $this->assertSame( $last, $func );
+                       $last = $call;
+               }
+               $this->assertSame( $last, 'bar@1' );
+       }
+
+       /**
+        * Get an Xhprof instance that has been primed with a set of known testing
+        * data. Tests for the Xhprof class should laregly be concerned with
+        * evaluating the manipulations of the data collected by xhprof rather
+        * than the data collection process itself.
+        *
+        * The returned Xhprof instance primed will be with a data set created by
+        * running this trivial program using the PECL xhprof implementation:
+        * @code
+        * function bar( $x ) {
+        *   if ( $x > 0 ) {
+        *     bar($x - 1);
+        *   }
+        * }
+        * function foo() {
+        *   for ( $idx = 0; $idx < 2; $idx++ ) {
+        *     bar( $idx );
+        *     $x = strlen( 'abc' );
+        *   }
+        * }
+        * xhprof_enable( XHPROF_FLAGS_CPU | XHPROF_FLAGS_MEMORY );
+        * foo();
+        * $x = xhprof_disable();
+        * var_export( $x );
+        * @endcode
+        *
+        * @return Xhprof
+        */
+       protected function getXhprofDataFixture( array $opts = [] ) {
+               return new XhprofData( [
+                       'foo==>bar' => [
+                               'ct' => 2,
+                               'wt' => 57,
+                               'cpu' => 92,
+                               'mu' => 1896,
+                               'pmu' => 0,
+                       ],
+                       'foo==>strlen' => [
+                               'ct' => 2,
+                               'wt' => 21,
+                               'cpu' => 141,
+                               'mu' => 752,
+                               'pmu' => 0,
+                       ],
+                       'bar==>bar@1' => [
+                               'ct' => 1,
+                               'wt' => 18,
+                               'cpu' => 19,
+                               'mu' => 752,
+                               'pmu' => 0,
+                       ],
+                       'main()==>foo' => [
+                               'ct' => 1,
+                               'wt' => 304,
+                               'cpu' => 307,
+                               'mu' => 4008,
+                               'pmu' => 0,
+                       ],
+                       'main()==>xhprof_disable' => [
+                               'ct' => 1,
+                               'wt' => 8,
+                               'cpu' => 10,
+                               'mu' => 768,
+                               'pmu' => 392,
+                       ],
+                       'main()' => [
+                               'ct' => 1,
+                               'wt' => 353,
+                               'cpu' => 351,
+                               'mu' => 6112,
+                               'pmu' => 1424,
+                       ],
+               ], $opts );
+       }
+
+       /**
+        * Assert that the given array has the described structure.
+        *
+        * @param array $struct Array of key => type mappings
+        * @param array $actual Array to check
+        * @param string $label
+        */
+       protected function assertArrayStructure( $struct, $actual, $label = null ) {
+               $this->assertInternalType( 'array', $actual, $label );
+               $this->assertCount( count( $struct ), $actual, $label );
+               foreach ( $struct as $key => $type ) {
+                       $this->assertArrayHasKey( $key, $actual );
+                       $this->assertInternalType( $type, $actual[$key] );
+               }
+       }
+}
diff --git a/tests/phpunit/unit/includes/libs/XhprofTest.php b/tests/phpunit/unit/includes/libs/XhprofTest.php
new file mode 100644 (file)
index 0000000..ccad4a4
--- /dev/null
@@ -0,0 +1,113 @@
+<?php
+/**
+ * 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; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * 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.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+class XhprofTest extends PHPUnit\Framework\TestCase {
+
+       use MediaWikiCoversValidator;
+
+       /**
+        * Trying to enable Xhprof when it is already enabled causes an exception
+        * to be thrown.
+        *
+        * @expectedException        Exception
+        * @expectedExceptionMessage already enabled
+        * @covers Xhprof::enable
+        */
+       public function testEnable() {
+               $xhprof = new ReflectionClass( Xhprof::class );
+               $enabled = $xhprof->getProperty( 'enabled' );
+               $enabled->setAccessible( true );
+               $enabled->setValue( true );
+               $xhprof->getMethod( 'enable' )->invoke( null );
+       }
+
+       /**
+        * callAny() calls the first function of the list.
+        *
+        * @covers Xhprof::callAny
+        * @dataProvider provideCallAny
+        */
+       public function testCallAny( array $functions, array $args, $expectedResult ) {
+               $xhprof = new ReflectionClass( Xhprof::class );
+               $callAny = $xhprof->getMethod( 'callAny' );
+               $callAny->setAccessible( true );
+
+               $this->assertEquals( $expectedResult,
+                       $callAny->invoke( null, $functions, $args ) );
+       }
+
+       /**
+        * Data provider for testCallAny().
+       */
+       public function provideCallAny() {
+               return [
+                       [
+                               [ 'wfTestCallAny_func1', 'wfTestCallAny_func2', 'wfTestCallAny_func3' ],
+                               [ 3, 4 ],
+                               12
+                       ],
+                       [
+                               [ 'wfTestCallAny_nosuchfunc1', 'wfTestCallAny_func2', 'wfTestCallAny_func3' ],
+                               [ 3, 4 ],
+                               7
+                       ],
+                       [
+                               [ 'wfTestCallAny_nosuchfunc1', 'wfTestCallAny_nosuchfunc2', 'wfTestCallAny_func3' ],
+                               [ 3, 4 ],
+                               -1
+                       ]
+
+               ];
+       }
+
+       /**
+        * callAny() throws an exception when all functions are unavailable.
+        *
+        * @expectedException        Exception
+        * @expectedExceptionMessage Neither xhprof nor tideways are installed
+        * @covers Xhprof::callAny
+        */
+       public function testCallAnyNoneAvailable() {
+               $xhprof = new ReflectionClass( Xhprof::class );
+               $callAny = $xhprof->getMethod( 'callAny' );
+               $callAny->setAccessible( true );
+
+               $callAny->invoke( $xhprof, [
+                       'wfTestCallAny_nosuchfunc1',
+                       'wfTestCallAny_nosuchfunc2',
+                       'wfTestCallAny_nosuchfunc3'
+               ] );
+       }
+}
+
+/** Test function #1 for XhprofTest::testCallAny */
+function wfTestCallAny_func1( $a, $b ) {
+       return $a * $b;
+}
+
+/** Test function #2 for XhprofTest::testCallAny */
+function wfTestCallAny_func2( $a, $b ) {
+       return $a + $b;
+}
+
+/** Test function #3 for XhprofTest::testCallAny */
+function wfTestCallAny_func3( $a, $b ) {
+       return $a - $b;
+}
diff --git a/tests/phpunit/unit/includes/libs/XmlTypeCheckTest.php b/tests/phpunit/unit/includes/libs/XmlTypeCheckTest.php
new file mode 100644 (file)
index 0000000..8616b41
--- /dev/null
@@ -0,0 +1,79 @@
+<?php
+/**
+ * PHPUnit tests for XMLTypeCheck.
+ * @author physikerwelt
+ * @group Xml
+ * @covers XMLTypeCheck
+ */
+class XmlTypeCheckTest extends PHPUnit\Framework\TestCase {
+
+       use MediaWikiCoversValidator;
+
+       const WELL_FORMED_XML = "<root><child /></root>";
+       const MAL_FORMED_XML = "<root><child /></error>";
+       // phpcs:ignore Generic.Files.LineLength
+       const XML_WITH_PIH = '<?xml version="1.0"?><?xml-stylesheet type="text/xsl" href="/w/index.php"?><svg><child /></svg>';
+
+       /**
+        * @covers XMLTypeCheck::newFromString
+        * @covers XMLTypeCheck::getRootElement
+        */
+       public function testWellFormedXML() {
+               $testXML = XmlTypeCheck::newFromString( self::WELL_FORMED_XML );
+               $this->assertTrue( $testXML->wellFormed );
+               $this->assertEquals( 'root', $testXML->getRootElement() );
+       }
+
+       /**
+        * @covers XMLTypeCheck::newFromString
+        */
+       public function testMalFormedXML() {
+               $testXML = XmlTypeCheck::newFromString( self::MAL_FORMED_XML );
+               $this->assertFalse( $testXML->wellFormed );
+       }
+
+       /**
+        * Verify we check for recursive entity DOS
+        *
+        * (If the DOS isn't properly handled, the test runner will probably go OOM...)
+        */
+       public function testRecursiveEntity() {
+               $xml = <<<'XML'
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE foo [
+       <!ENTITY test "&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;">
+       <!ENTITY a "&b;&b;&b;&b;&b;&b;&b;&b;&b;&b;&b;&b;&b;&b;&b;&b;&b;&b;&b;&b;&b;&b;&b;&b;">
+       <!ENTITY b "&c;&c;&c;&c;&c;&c;&c;&c;&c;&c;&c;&c;&c;&c;&c;&c;&c;&c;&c;&c;&c;&c;&c;&c;">
+       <!ENTITY c "&d;&d;&d;&d;&d;&d;&d;&d;&d;&d;&d;&d;&d;&d;&d;&d;&d;&d;&d;&d;&d;&d;&d;&d;">
+       <!ENTITY d "&e;&e;&e;&e;&e;&e;&e;&e;&e;&e;&e;&e;&e;&e;&e;&e;&e;&e;&e;&e;&e;&e;&e;&e;">
+       <!ENTITY e "&f;&f;&f;&f;&f;&f;&f;&f;&f;&f;&f;&f;&f;&f;&f;&f;&f;&f;&f;&f;&f;&f;&f;&f;">
+       <!ENTITY f "&g;&g;&g;&g;&g;&g;&g;&g;&g;&g;&g;&g;&g;&g;&g;&g;&g;&g;&g;&g;&g;&g;&g;&g;">
+       <!ENTITY g "-00000000000000000000000000000000000000000000000000000000000000000000000-">
+]>
+<foo>
+<bar>&test;</bar>
+</foo>
+XML;
+               $check = XmlTypeCheck::newFromString( $xml );
+               $this->assertFalse( $check->wellFormed );
+       }
+
+       /**
+        * @covers XMLTypeCheck::processingInstructionHandler
+        */
+       public function testProcessingInstructionHandler() {
+               $called = false;
+               $testXML = new XmlTypeCheck(
+                       self::XML_WITH_PIH,
+                       null,
+                       false,
+                       [
+                               'processing_instruction_handler' => function () use ( &$called ) {
+                                       $called = true;
+                               }
+                       ]
+               );
+               $this->assertTrue( $called );
+       }
+
+}
diff --git a/tests/phpunit/unit/includes/libs/composer/ComposerInstalledTest.php b/tests/phpunit/unit/includes/libs/composer/ComposerInstalledTest.php
new file mode 100644 (file)
index 0000000..d94cc45
--- /dev/null
@@ -0,0 +1,498 @@
+<?php
+
+class ComposerInstalledTest extends PHPUnit\Framework\TestCase {
+
+       private $installed;
+
+       public function setUp() {
+               parent::setUp();
+               $this->installed = __DIR__ . "/../../../../data/composer/installed.json";
+       }
+
+       /**
+        * @covers ComposerInstalled::__construct
+        * @covers ComposerInstalled::getInstalledDependencies
+        */
+       public function testGetInstalledDependencies() {
+               $installed = new ComposerInstalled( $this->installed );
+               $this->assertEquals( [
+               'leafo/lessphp' => [
+                       'version' => '0.5.0',
+                       'type' => 'library',
+                       'licenses' => [ 'MIT', 'GPL-3.0-only' ],
+                       'authors' => [
+                               [
+                                       'name' => 'Leaf Corcoran',
+                                       'email' => 'leafot@gmail.com',
+                                       'homepage' => 'http://leafo.net',
+                               ],
+                       ],
+                       'description' => 'lessphp is a compiler for LESS written in PHP.',
+               ],
+               'psr/log' => [
+                       'version' => '1.0.0',
+                       'type' => 'library',
+                       'licenses' => [ 'MIT' ],
+                       'authors' => [
+                               [
+                                       'name' => 'PHP-FIG',
+                                       'homepage' => 'http://www.php-fig.org/',
+                               ],
+                       ],
+                       'description' => 'Common interface for logging libraries',
+               ],
+               'cssjanus/cssjanus' => [
+                       'version' => '1.1.1',
+                       'type' => 'library',
+                       'licenses' => [ 'Apache-2.0' ],
+                       'authors' => [
+                       ],
+                       'description' => 'Convert CSS stylesheets between left-to-right ' .
+                               'and right-to-left.',
+               ],
+               'cdb/cdb' => [
+                       'version' => '1.0.0',
+                       'type' => 'library',
+                       'licenses' => [ 'GPLv2' ],
+                       'authors' => [
+                               [
+                                       'name' => 'Tim Starling',
+                                       'email' => 'tstarling@wikimedia.org',
+                               ],
+                               [
+                                       'name' => 'Chad Horohoe',
+                                       'email' => 'chad@wikimedia.org',
+                               ],
+                       ],
+                       'description' => 'Constant Database (CDB) wrapper library for PHP. ' .
+                               'Provides pure-PHP fallback when dba_* functions are absent.',
+               ],
+               'sebastian/version' => [
+                       'version' => '2.0.1',
+                       'type' => 'library',
+                       'licenses' => [ 'BSD-3-Clause' ],
+                       'authors' => [
+                               [
+                                       'name' => 'Sebastian Bergmann',
+                                       'email' => 'sebastian@phpunit.de',
+                                       'role' => 'lead',
+                               ],
+                       ],
+                       'description' => 'Library that helps with managing the version ' .
+                               'number of Git-hosted PHP projects',
+               ],
+               'sebastian/resource-operations' => [
+                       'version' => '1.0.0',
+                       'type' => 'library',
+                       'licenses' => [ 'BSD-3-Clause' ],
+                       'authors' => [
+                               [
+                                       'name' => 'Sebastian Bergmann',
+                                       'email' => 'sebastian@phpunit.de',
+                               ],
+                       ],
+                       'description' => 'Provides a list of PHP built-in functions that ' .
+                               'operate on resources',
+               ],
+               'sebastian/recursion-context' => [
+                       'version' => '3.0.0',
+                       'type' => 'library',
+                       'licenses' => [ 'BSD-3-Clause' ],
+                       'authors' => [
+                               [
+                                       'name' => 'Jeff Welch',
+                                       'email' => 'whatthejeff@gmail.com',
+                               ],
+                               [
+                                       'name' => 'Sebastian Bergmann',
+                                       'email' => 'sebastian@phpunit.de',
+                               ],
+                               [
+                                       'name' => 'Adam Harvey',
+                                       'email' => 'aharvey@php.net',
+                               ],
+                       ],
+                       'description' => 'Provides functionality to recursively process PHP ' .
+                               'variables',
+               ],
+               'sebastian/object-reflector' => [
+                       'version' => '1.1.1',
+                       'type' => 'library',
+                       'licenses' => [ 'BSD-3-Clause' ],
+                       'authors' => [
+                               [
+                                       'name' => 'Sebastian Bergmann',
+                                       'email' => 'sebastian@phpunit.de',
+                               ],
+                       ],
+                       'description' => 'Allows reflection of object attributes, including ' .
+                               'inherited and non-public ones',
+               ],
+               'sebastian/object-enumerator' => [
+                       'version' => '3.0.3',
+                       'type' => 'library',
+                       'licenses' => [ 'BSD-3-Clause' ],
+                       'authors' => [
+                               [
+                                       'name' => 'Sebastian Bergmann',
+                                       'email' => 'sebastian@phpunit.de',
+                               ],
+                       ],
+                       'description' => 'Traverses array structures and object graphs ' .
+                               'to enumerate all referenced objects',
+               ],
+               'sebastian/global-state' => [
+                       'version' => '2.0.0',
+                       'type' => 'library',
+                       'licenses' => [ 'BSD-3-Clause' ],
+                       'authors' => [
+                               [
+                                       'name' => 'Sebastian Bergmann',
+                                       'email' => 'sebastian@phpunit.de',
+                               ],
+                       ],
+                       'description' => 'Snapshotting of global state',
+               ],
+               'sebastian/exporter' => [
+                       'version' => '3.1.0',
+                       'type' => 'library',
+                       'licenses' => [ 'BSD-3-Clause' ],
+                       'authors' => [
+                               [
+                                       'name' => 'Jeff Welch',
+                                       'email' => 'whatthejeff@gmail.com',
+                               ],
+                               [
+                                       'name' => 'Volker Dusch',
+                                       'email' => 'github@wallbash.com',
+                               ],
+                               [
+                                       'name' => 'Bernhard Schussek',
+                                       'email' => 'bschussek@2bepublished.at',
+                               ],
+                               [
+                                       'name' => 'Sebastian Bergmann',
+                                       'email' => 'sebastian@phpunit.de',
+                               ],
+                               [
+                                       'name' => 'Adam Harvey',
+                                       'email' => 'aharvey@php.net',
+                               ],
+                       ],
+                       'description' => 'Provides the functionality to export PHP ' .
+                               'variables for visualization',
+               ],
+               'sebastian/environment' => [
+                       'version' => '3.1.0',
+                       'type' => 'library',
+                       'licenses' => [ 'BSD-3-Clause' ],
+                       'authors' => [
+                               [
+                                       'name' => 'Sebastian Bergmann',
+                                       'email' => 'sebastian@phpunit.de',
+                               ],
+                       ],
+                       'description' => 'Provides functionality to handle HHVM/PHP ' .
+                               'environments',
+               ],
+               'sebastian/diff' => [
+                       'version' => '2.0.1',
+                       'type' => 'library',
+                       'licenses' => [ 'BSD-3-Clause' ],
+                       'authors' => [
+                               [
+                                       'name' => 'Kore Nordmann',
+                                       'email' => 'mail@kore-nordmann.de',
+                               ],
+                               [
+                                       'name' => 'Sebastian Bergmann',
+                                       'email' => 'sebastian@phpunit.de',
+                               ],
+                       ],
+                       'description' => 'Diff implementation',
+               ],
+               'sebastian/comparator' => [
+                       'version' => '2.1.1',
+                       'type' => 'library',
+                       'licenses' => [ 'BSD-3-Clause' ],
+                       'authors' => [
+                               [
+                                       'name' => 'Jeff Welch',
+                                       'email' => 'whatthejeff@gmail.com',
+                               ],
+                               [
+                                       'name' => 'Volker Dusch',
+                                       'email' => 'github@wallbash.com',
+                               ],
+                               [
+                                       'name' => 'Bernhard Schussek',
+                                       'email' => 'bschussek@2bepublished.at',
+                               ],
+                               [
+                                       'name' => 'Sebastian Bergmann',
+                                       'email' => 'sebastian@phpunit.de',
+                               ],
+                       ],
+                       'description' => 'Provides the functionality to compare PHP ' .
+                               'values for equality',
+               ],
+               'doctrine/instantiator' => [
+                       'version' => '1.1.0',
+                       'type' => 'library',
+                       'licenses' => [ 'MIT' ],
+                       'authors' => [
+                               [
+                                       'name' => 'Marco Pivetta',
+                                       'email' => 'ocramius@gmail.com',
+                                       'homepage' => 'http://ocramius.github.com/',
+                               ],
+                       ],
+                       'description' => 'A small, lightweight utility to instantiate ' .
+                               'objects in PHP without invoking their constructors',
+               ],
+               'phpunit/php-text-template' => [
+                       'version' => '1.2.1',
+                       'type' => 'library',
+                       'licenses' => [ 'BSD-3-Clause' ],
+                       'authors' => [
+                               [
+                                       'name' => 'Sebastian Bergmann',
+                                       'email' => 'sebastian@phpunit.de',
+                                       'role' => 'lead',
+                               ],
+                       ],
+                       'description' => 'Simple template engine.',
+               ],
+               'phpunit/phpunit-mock-objects' => [
+                       'version' => '5.0.6',
+                       'type' => 'library',
+                       'licenses' => [ 'BSD-3-Clause' ],
+                       'authors' => [
+                               [
+                                       'name' => 'Sebastian Bergmann',
+                                       'email' => 'sebastian@phpunit.de',
+                                       'role' => 'lead',
+                               ],
+                       ],
+                       'description' => 'Mock Object library for PHPUnit',
+               ],
+               'phpunit/php-timer' => [
+                       'version' => '1.0.9',
+                       'type' => 'library',
+                       'licenses' => [ 'BSD-3-Clause' ],
+                       'authors' => [
+                               [
+                                       'name' => 'Sebastian Bergmann',
+                                       'email' => 'sb@sebastian-bergmann.de',
+                                       'role' => 'lead',
+                               ],
+                       ],
+                       'description' => 'Utility class for timing',
+               ],
+               'phpunit/php-file-iterator' => [
+                       'version' => '1.4.5',
+                       'type' => 'library',
+                       'licenses' => [ 'BSD-3-Clause' ],
+                       'authors' => [
+                               [
+                                       'name' => 'Sebastian Bergmann',
+                                       'email' => 'sb@sebastian-bergmann.de',
+                                       'role' => 'lead',
+                               ],
+                       ],
+                       'description' => 'FilterIterator implementation that filters ' .
+                               'files based on a list of suffixes.',
+               ],
+               'theseer/tokenizer' => [
+                       'version' => '1.1.0',
+                       'type' => 'library',
+                       'licenses' => [ 'BSD-3-Clause' ],
+                       'authors' => [
+                               [
+                                       'name' => 'Arne Blankerts',
+                                       'email' => 'arne@blankerts.de',
+                                       'role' => 'Developer',
+                               ],
+                       ],
+                       'description' => 'A small library for converting tokenized PHP ' .
+                               'source code into XML and potentially other formats',
+               ],
+               'sebastian/code-unit-reverse-lookup' => [
+                       'version' => '1.0.1',
+                       'type' => 'library',
+                       'licenses' => [ 'BSD-3-Clause' ],
+                       'authors' => [
+                               [
+                                       'name' => 'Sebastian Bergmann',
+                                       'email' => 'sebastian@phpunit.de',
+                               ],
+                       ],
+                       'description' => 'Looks up which function or method a line of ' .
+                               'code belongs to',
+               ],
+               'phpunit/php-token-stream' => [
+                       'version' => '2.0.2',
+                       'type' => 'library',
+                       'licenses' => [ 'BSD-3-Clause' ],
+                       'authors' => [
+                               [
+                                       'name' => 'Sebastian Bergmann',
+                                       'email' => 'sebastian@phpunit.de',
+                               ],
+                       ],
+                       'description' => 'Wrapper around PHP\'s tokenizer extension.',
+               ],
+               'phpunit/php-code-coverage' => [
+                       'version' => '5.3.0',
+                       'type' => 'library',
+                       'licenses' => [ 'BSD-3-Clause' ],
+                       'authors' => [
+                               [
+                                       'name' => 'Sebastian Bergmann',
+                                       'email' => 'sebastian@phpunit.de',
+                                       'role' => 'lead',
+                               ],
+                       ],
+                       'description' => 'Library that provides collection, processing, ' .
+                               'and rendering functionality for PHP code coverage information.',
+               ],
+               'webmozart/assert' => [
+                       'version' => '1.2.0',
+                       'type' => 'library',
+                       'licenses' => [ 'MIT' ],
+                       'authors' => [
+                               [
+                                       'name' => 'Bernhard Schussek',
+                                       'email' => 'bschussek@gmail.com',
+                               ],
+                       ],
+                       'description' => 'Assertions to validate method input/output with ' .
+                               'nice error messages.',
+               ],
+               'phpdocumentor/reflection-common' => [
+                       'version' => '1.0.1',
+                       'type' => 'library',
+                       'licenses' => [ 'MIT' ],
+                       'authors' => [
+                               [
+                                       'name' => 'Jaap van Otterdijk',
+                                       'email' => 'opensource@ijaap.nl',
+                               ],
+                       ],
+                       'description' => 'Common reflection classes used by phpdocumentor to ' .
+                               'reflect the code structure',
+               ],
+               'phpdocumentor/type-resolver' => [
+                       'version' => '0.4.0',
+                       'type' => 'library',
+                       'licenses' => [ 'MIT' ],
+                       'authors' => [
+                               [
+                                       'name' => 'Mike van Riel',
+                                       'email' => 'me@mikevanriel.com',
+                               ],
+                       ],
+                       'description' => '',
+               ],
+               'phpdocumentor/reflection-docblock' => [
+                       'version' => '4.2.0',
+                       'type' => 'library',
+                       'licenses' => [ 'MIT' ],
+                       'authors' => [
+                               [
+                                       'name' => 'Mike van Riel',
+                                       'email' => 'me@mikevanriel.com',
+                               ],
+                       ],
+                       'description' => 'With this component, a library can provide support for ' .
+                               'annotations via DocBlocks or otherwise retrieve information that ' .
+                               'is embedded in a DocBlock.',
+               ],
+               'phpspec/prophecy' => [
+                       'version' => '1.7.3',
+                       'type' => 'library',
+                       'licenses' => [ 'MIT' ],
+                       'authors' => [
+                               [
+                                       'name' => 'Konstantin Kudryashov',
+                                       'email' => 'ever.zet@gmail.com',
+                                       'homepage' => 'http://everzet.com',
+                               ],
+                               [
+                                       'name' => 'Marcello Duarte',
+                                       'email' => 'marcello.duarte@gmail.com',
+                               ],
+                       ],
+                       'description' => 'Highly opinionated mocking framework for PHP 5.3+',
+               ],
+               'phar-io/version' => [
+                       'version' => '1.0.1',
+                       'type' => 'library',
+                       'licenses' => [ 'BSD-3-Clause' ],
+                       'authors' => [
+                               [
+                                       'name' => 'Arne Blankerts',
+                                       'email' => 'arne@blankerts.de',
+                                       'role' => 'Developer',
+                               ],
+                               [
+                                       'name' => 'Sebastian Heuer',
+                                       'email' => 'sebastian@phpeople.de',
+                                       'role' => 'Developer',
+                               ],
+                               [
+                                       'name' => 'Sebastian Bergmann',
+                                       'email' => 'sebastian@phpunit.de',
+                                       'role' => 'Developer',
+                               ],
+                       ],
+                       'description' => 'Library for handling version information and constraints',
+               ],
+               'phar-io/manifest' => [
+                       'version' => '1.0.1',
+                       'type' => 'library',
+                       'licenses' => [ 'BSD-3-Clause' ],
+                       'authors' => [
+                               [
+                                       'name' => 'Arne Blankerts',
+                                       'email' => 'arne@blankerts.de',
+                                       'role' => 'Developer',
+                               ],
+                               [
+                                       'name' => 'Sebastian Heuer',
+                                       'email' => 'sebastian@phpeople.de',
+                                       'role' => 'Developer',
+                               ],
+                               [
+                                       'name' => 'Sebastian Bergmann',
+                                       'email' => 'sebastian@phpunit.de',
+                                       'role' => 'Developer',
+                               ],
+                       ],
+                       'description' => 'Component for reading phar.io manifest ' .
+                               'information from a PHP Archive (PHAR)',
+               ],
+               'myclabs/deep-copy' => [
+                       'version' => '1.7.0',
+                       'type' => 'library',
+                       'licenses' => [ 'MIT' ],
+                       'authors' => [
+                       ],
+                       'description' => 'Create deep copies (clones) of your objects',
+               ],
+               'phpunit/phpunit' => [
+                       'version' => '6.5.5',
+                       'type' => 'library',
+                       'licenses' => [ 'BSD-3-Clause' ],
+                       'authors' => [
+                               [
+                                       'name' => 'Sebastian Bergmann',
+                                       'email' => 'sebastian@phpunit.de',
+                                       'role' => 'lead',
+                               ],
+                       ],
+                       'description' => 'The PHP Unit Testing framework.',
+               ],
+               ], $installed->getInstalledDependencies() );
+       }
+}
diff --git a/tests/phpunit/unit/includes/libs/composer/ComposerJsonTest.php b/tests/phpunit/unit/includes/libs/composer/ComposerJsonTest.php
new file mode 100644 (file)
index 0000000..a009a51
--- /dev/null
@@ -0,0 +1,41 @@
+<?php
+
+class ComposerJsonTest extends PHPUnit\Framework\TestCase {
+
+       private $json, $json2;
+
+       public function setUp() {
+               parent::setUp();
+               $this->json = __DIR__ . "/../../../../data/composer/composer.json";
+               $this->json2 = __DIR__ . "/../../../../data/composer/new-composer.json";
+       }
+
+       /**
+        * @covers ComposerJson::__construct
+        * @covers ComposerJson::getRequiredDependencies
+        */
+       public function testGetRequiredDependencies() {
+               $json = new ComposerJson( $this->json );
+               $this->assertEquals( [
+                       'cdb/cdb' => '1.0.0',
+                       'cssjanus/cssjanus' => '1.1.1',
+                       'leafo/lessphp' => '0.5.0',
+                       'psr/log' => '1.0.0',
+               ], $json->getRequiredDependencies() );
+       }
+
+       public static function provideNormalizeVersion() {
+               return [
+                       [ 'v1.0.0', '1.0.0' ],
+                       [ '0.0.5', '0.0.5' ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideNormalizeVersion
+        * @covers ComposerJson::normalizeVersion
+        */
+       public function testNormalizeVersion( $input, $expected ) {
+               $this->assertEquals( $expected, ComposerJson::normalizeVersion( $input ) );
+       }
+}
diff --git a/tests/phpunit/unit/includes/libs/composer/ComposerLockTest.php b/tests/phpunit/unit/includes/libs/composer/ComposerLockTest.php
new file mode 100644 (file)
index 0000000..90c036a
--- /dev/null
@@ -0,0 +1,120 @@
+<?php
+
+class ComposerLockTest extends PHPUnit\Framework\TestCase {
+
+       private $lock;
+
+       public function setUp() {
+               parent::setUp();
+               $this->lock = __DIR__ . "/../../../../data/composer/composer.lock";
+       }
+
+       /**
+        * @covers ComposerLock::__construct
+        * @covers ComposerLock::getInstalledDependencies
+        */
+       public function testGetInstalledDependencies() {
+               $lock = new ComposerLock( $this->lock );
+               $this->assertEquals( [
+                       'wikimedia/cdb' => [
+                               'version' => '1.0.1',
+                               'type' => 'library',
+                               'licenses' => [ 'GPL-2.0-only' ],
+                               'authors' => [
+                                       [
+                                               'name' => 'Tim Starling',
+                                               'email' => 'tstarling@wikimedia.org',
+                                       ],
+                                       [
+                                               'name' => 'Chad Horohoe',
+                                               'email' => 'chad@wikimedia.org',
+                                       ],
+                               ],
+                               'description' => 'Constant Database (CDB) wrapper library for PHP. ' .
+                                       'Provides pure-PHP fallback when dba_* functions are absent.',
+                       ],
+                       'cssjanus/cssjanus' => [
+                               'version' => '1.1.1',
+                               'type' => 'library',
+                               'licenses' => [ 'Apache-2.0' ],
+                               'authors' => [],
+                               'description' => 'Convert CSS stylesheets between left-to-right and right-to-left.',
+                       ],
+                       'leafo/lessphp' => [
+                               'version' => '0.5.0',
+                               'type' => 'library',
+                               'licenses' => [ 'MIT', 'GPL-3.0-only' ],
+                               'authors' => [
+                                       [
+                                               'name' => 'Leaf Corcoran',
+                                               'email' => 'leafot@gmail.com',
+                                               'homepage' => 'http://leafo.net',
+                                       ],
+                               ],
+                               'description' => 'lessphp is a compiler for LESS written in PHP.',
+                       ],
+                       'psr/log' => [
+                               'version' => '1.0.0',
+                               'type' => 'library',
+                               'licenses' => [ 'MIT' ],
+                               'authors' => [
+                                       [
+                                               'name' => 'PHP-FIG',
+                                               'homepage' => 'http://www.php-fig.org/',
+                                       ],
+                               ],
+                               'description' => 'Common interface for logging libraries',
+                       ],
+                       'oojs/oojs-ui' => [
+                               'version' => '0.6.0',
+                               'type' => 'library',
+                               'licenses' => [ 'MIT' ],
+                               'authors' => [],
+                               'description' => '',
+                       ],
+                       'composer/installers' => [
+                               'version' => '1.0.19',
+                               'type' => 'composer-installer',
+                               'licenses' => [ 'MIT' ],
+                               'authors' => [
+                                       [
+                                               'name' => 'Kyle Robinson Young',
+                                               'email' => 'kyle@dontkry.com',
+                                               'homepage' => 'https://github.com/shama',
+                                       ],
+                               ],
+                               'description' => 'A multi-framework Composer library installer',
+                       ],
+                       'mediawiki/translate' => [
+                               'version' => '2014.12',
+                               'type' => 'mediawiki-extension',
+                               'licenses' => [ 'GPL-2.0-or-later' ],
+                               'authors' => [
+                                       [
+                                               'name' => 'Niklas Laxström',
+                                               'email' => 'niklas.laxstrom@gmail.com',
+                                               'role' => 'Lead nitpicker',
+                                       ],
+                                       [
+                                               'name' => 'Siebrand Mazeland',
+                                               'email' => 's.mazeland@xs4all.nl',
+                                               'role' => 'Developer',
+                                       ],
+                               ],
+                               'description' => 'The only standard solution to translate any kind ' .
+                                       'of text with an avant-garde web interface within MediaWiki, ' .
+                                       'including your documentation and software',
+                       ],
+                       'mediawiki/universal-language-selector' => [
+                               'version' => '2014.12',
+                               'type' => 'mediawiki-extension',
+                               'licenses' => [ 'GPL-2.0-or-later', 'MIT' ],
+                               'authors' => [],
+                               'description' => 'The primary aim is to allow users to select a language ' .
+                                       'and configure its support in an easy way. ' .
+                                       'Main features are language selection, input methods and web fonts.',
+                       ],
+               ], $lock->getInstalledDependencies() );
+       }
+
+}
diff --git a/tests/phpunit/unit/includes/libs/http/HttpAcceptNegotiatorTest.php b/tests/phpunit/unit/includes/libs/http/HttpAcceptNegotiatorTest.php
new file mode 100644 (file)
index 0000000..02eac11
--- /dev/null
@@ -0,0 +1,150 @@
+<?php
+
+use Wikimedia\Http\HttpAcceptNegotiator;
+
+/**
+ * @covers Wikimedia\Http\HttpAcceptNegotiator
+ *
+ * @author Daniel Kinzler
+ */
+class HttpAcceptNegotiatorTest extends \PHPUnit\Framework\TestCase {
+
+       public function provideGetFirstSupportedValue() {
+               return [
+                       [ // #0: empty
+                               [], // supported
+                               [], // accepted
+                               null, // default
+                               null,  // expected
+                       ],
+                       [ // #1: simple
+                               [ 'text/foo', 'text/BAR', 'application/zuul' ], // supported
+                               [ 'text/xzy', 'text/bar' ], // accepted
+                               null, // default
+                               'text/BAR',  // expected
+                       ],
+                       [ // #2: default
+                               [ 'text/foo', 'text/BAR', 'application/zuul' ], // supported
+                               [ 'text/xzy', 'text/xoo' ], // accepted
+                               'X', // default
+                               'X',  // expected
+                       ],
+                       [ // #3: preference
+                               [ 'text/foo', 'text/bar', 'application/zuul' ], // supported
+                               [ 'text/xoo', 'text/BAR', 'text/foo' ], // accepted
+                               null, // default
+                               'text/bar',  // expected
+                       ],
+                       [ // #4: * wildcard
+                               [ 'text/foo', 'text/BAR', 'application/zuul' ], // supported
+                               [ 'text/xoo', '*' ], // accepted
+                               null, // default
+                               'text/foo',  // expected
+                       ],
+                       [ // #5: */* wildcard
+                               [ 'text/foo', 'text/BAR', 'application/zuul' ], // supported
+                               [ 'text/xoo', '*/*' ], // accepted
+                               null, // default
+                               'text/foo',  // expected
+                       ],
+                       [ // #6: text/* wildcard
+                               [ 'text/foo', 'text/BAR', 'application/zuul' ], // supported
+                               [ 'application/*', 'text/foo' ], // accepted
+                               null, // default
+                               'application/zuul',  // expected
+                       ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideGetFirstSupportedValue
+        */
+       public function testGetFirstSupportedValue( $supported, $accepted, $default, $expected ) {
+               $negotiator = new HttpAcceptNegotiator( $supported );
+               $actual = $negotiator->getFirstSupportedValue( $accepted, $default );
+
+               $this->assertEquals( $expected, $actual );
+       }
+
+       public function provideGetBestSupportedKey() {
+               return [
+                       [ // #0: empty
+                               [], // supported
+                               [], // accepted
+                               null, // default
+                               null,  // expected
+                       ],
+                       [ // #1: simple
+                               [ 'text/foo', 'text/BAR', 'application/zuul' ], // supported
+                               [ 'text/xzy' => 1, 'text/bar' => 0.5 ], // accepted
+                               null, // default
+                               'text/BAR',  // expected
+                       ],
+                       [ // #2: default
+                               [ 'text/foo', 'text/BAR', 'application/zuul' ], // supported
+                               [ 'text/xzy' => 1, 'text/xoo' => 0.5 ], // accepted
+                               'X', // default
+                               'X',  // expected
+                       ],
+                       [ // #3: weighted
+                               [ 'text/foo', 'text/BAR', 'application/zuul' ], // supported
+                               [ 'text/foo' => 0.3, 'text/BAR' => 0.8, 'application/zuul' => 0.5 ], // accepted
+                               null, // default
+                               'text/BAR',  // expected
+                       ],
+                       [ // #4: zero weight
+                               [ 'text/foo', 'text/BAR', 'application/zuul' ], // supported
+                               [ 'text/foo' => 0, 'text/xoo' => 1 ], // accepted
+                               null, // default
+                               null,  // expected
+                       ],
+                       [ // #5: * wildcard
+                               [ 'text/foo', 'text/BAR', 'application/zuul' ], // supported
+                               [ 'text/xoo' => 0.5, '*' => 0.1 ], // accepted
+                               null, // default
+                               'text/foo',  // expected
+                       ],
+                       [ // #6: */* wildcard
+                               [ 'text/foo', 'text/BAR', 'application/zuul' ], // supported
+                               [ 'text/xoo' => 0.5, '*/*' => 0.1 ], // accepted
+                               null, // default
+                               'text/foo',  // expected
+                       ],
+                       [ // #7: text/* wildcard
+                               [ 'text/foo', 'text/BAR', 'application/zuul' ], // supported
+                               [ 'text/foo' => 0.3, 'application/*' => 0.8 ], // accepted
+                               null, // default
+                               'application/zuul',  // expected
+                       ],
+                       [ // #8: Test specific format preferred over wildcard (T133314)
+                               [ 'application/rdf+xml', 'text/json', 'text/html' ], // supported
+                               [ '*/*' => 1, 'text/html' => 1 ], // accepted
+                               null, // default
+                               'text/html',  // expected
+                       ],
+                       [ // #9: Test specific format preferred over range (T133314)
+                               [ 'application/rdf+xml', 'text/json', 'text/html' ], // supported
+                               [ 'text/*' => 1, 'text/html' => 1 ], // accepted
+                               null, // default
+                               'text/html',  // expected
+                       ],
+                       [ // #10: Test range preferred over wildcard (T133314)
+                               [ 'application/rdf+xml', 'text/html' ], // supported
+                               [ '*/*' => 1, 'text/*' => 1 ], // accepted
+                               null, // default
+                               'text/html',  // expected
+                       ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideGetBestSupportedKey
+        */
+       public function testGetBestSupportedKey( $supported, $accepted, $default, $expected ) {
+               $negotiator = new HttpAcceptNegotiator( $supported );
+               $actual = $negotiator->getBestSupportedKey( $accepted, $default );
+
+               $this->assertEquals( $expected, $actual );
+       }
+
+}
diff --git a/tests/phpunit/unit/includes/libs/http/HttpAcceptParserTest.php b/tests/phpunit/unit/includes/libs/http/HttpAcceptParserTest.php
new file mode 100644 (file)
index 0000000..e4b47b4
--- /dev/null
@@ -0,0 +1,56 @@
+<?php
+
+use Wikimedia\Http\HttpAcceptParser;
+
+/**
+ * @covers Wikimedia\Http\HttpAcceptParser
+ *
+ * @author Daniel Kinzler
+ */
+class HttpAcceptParserTest extends \PHPUnit\Framework\TestCase {
+
+       public function provideParseWeights() {
+               return [
+                       [ // #0
+                               '',
+                               []
+                       ],
+                       [ // #1
+                               'Foo/Bar',
+                               [ 'foo/bar' => 1 ]
+                       ],
+                       [ // #2
+                               'Accept: text/plain',
+                               [ 'text/plain' => 1 ]
+                       ],
+                       [ // #3
+                               'Accept: application/vnd.php.serialized, application/rdf+xml',
+                               [ 'application/vnd.php.serialized' => 1, 'application/rdf+xml' => 1 ]
+                       ],
+                       [ // #4
+                               'foo; q=0.2, xoo; q=0,text/n3',
+                               [ 'text/n3' => 1, 'foo' => 0.2 ]
+                       ],
+                       [ // #5
+                               '*; q=0.2, */*; q=0.1,text/*',
+                               [ 'text/*' => 1, '*' => 0.2, '*/*' => 0.1 ]
+                       ],
+                       // TODO: nicely ignore additional type paramerters
+                       //[ // #6
+                       //      'Foo; q=0.2, Xoo; level=3, Bar; charset=xyz; q=0.4',
+                       //      [ 'xoo' => 1, 'bar' => 0.4, 'foo' => 0.1 ]
+                       //],
+               ];
+       }
+
+       /**
+        * @dataProvider provideParseWeights
+        */
+       public function testParseWeights( $header, $expected ) {
+               $parser = new HttpAcceptParser();
+               $actual = $parser->parseWeights( $header );
+
+               $this->assertEquals( $expected, $actual ); // shouldn't be sensitive to order
+       }
+
+}
diff --git a/tests/phpunit/unit/includes/libs/mime/MSCompoundFileReaderTest.php b/tests/phpunit/unit/includes/libs/mime/MSCompoundFileReaderTest.php
new file mode 100644 (file)
index 0000000..7cc0525
--- /dev/null
@@ -0,0 +1,71 @@
+<?php
+/*
+ * Copyright 2019 Wikimedia 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.
+ */
+
+/**
+ * @group Media
+ * @covers MSCompoundFileReader
+ */
+class MSCompoundFileReaderTest extends PHPUnit\Framework\TestCase {
+
+       protected function setUp() {
+               parent::setUp();
+
+               if ( php_uname( 's' ) === 'Darwin' ) {
+                       $this->markTestSkipped(
+                               'T225019: Disable this test on macOS for now due to byte-order issues'
+                       );
+               }
+       }
+
+       public static function provideValid() {
+               return [
+                       [ 'calc.xls', 'application/vnd.ms-excel' ],
+                       [ 'excel2016-compat97.xls', 'application/vnd.ms-excel' ],
+                       [ 'gnumeric.xls', 'application/vnd.ms-excel' ],
+                       [ 'impress.ppt', 'application/vnd.ms-powerpoint' ],
+                       [ 'powerpoint2016-compat97.ppt', 'application/vnd.ms-powerpoint' ],
+                       [ 'word2016-compat97.doc', 'application/msword' ],
+                       [ 'writer.doc', 'application/msword' ],
+               ];
+       }
+
+       /** @dataProvider provideValid */
+       public function testReadFile( $fileName, $expectedMime ) {
+               global $IP;
+
+               $info = MSCompoundFileReader::readFile( "$IP/tests/phpunit/data/MSCompoundFileReader/$fileName" );
+               $this->assertTrue( $info['valid'] );
+               $this->assertSame( $expectedMime, $info['mime'] );
+       }
+
+       public static function provideInvalid() {
+               return [
+                       [ 'dir-beyond-end.xls', 'ERROR_READ_PAST_END' ],
+                       [ 'fat-loop.xls', 'ERROR_INVALID_FORMAT' ],
+                       [ 'invalid-signature.xls', 'ERROR_INVALID_SIGNATURE' ],
+               ];
+       }
+
+       /** @dataProvider provideInvalid */
+       public function testReadFileInvalid( $fileName, $expectedError ) {
+               global $IP;
+
+               $info = MSCompoundFileReader::readFile( "$IP/tests/phpunit/data/MSCompoundFileReader/$fileName" );
+               $this->assertFalse( $info['valid'] );
+               $this->assertSame( constant( MSCompoundFileReader::class . '::' . $expectedError ),
+                       $info['errorCode'] );
+       }
+}
diff --git a/tests/phpunit/unit/includes/libs/mime/MimeAnalyzerTest.php b/tests/phpunit/unit/includes/libs/mime/MimeAnalyzerTest.php
new file mode 100644 (file)
index 0000000..e78489d
--- /dev/null
@@ -0,0 +1,146 @@
+<?php
+/**
+ * @group Media
+ * @covers MimeAnalyzer
+ */
+class MimeAnalyzerTest extends PHPUnit\Framework\TestCase {
+
+       use MediaWikiCoversValidator;
+
+       /** @var MimeAnalyzer */
+       private $mimeAnalyzer;
+
+       function setUp() {
+               global $IP;
+
+               $this->mimeAnalyzer = new MimeAnalyzer( [
+                       'infoFile' => $IP . "/includes/libs/mime/mime.info",
+                       'typeFile' => $IP . "/includes/libs/mime/mime.types",
+                       'xmlTypes' => [
+                               'http://www.w3.org/2000/svg:svg' => 'image/svg+xml',
+                               'svg' => 'image/svg+xml',
+                               'http://www.lysator.liu.se/~alla/dia/:diagram' => 'application/x-dia-diagram',
+                               'http://www.w3.org/1999/xhtml:html' => 'text/html', // application/xhtml+xml?
+                               'html' => 'text/html', // application/xhtml+xml?
+                       ]
+               ] );
+               parent::setUp();
+       }
+
+       function doGuessMimeType( array $parameters = [] ) {
+               $class = new ReflectionClass( get_class( $this->mimeAnalyzer ) );
+               $method = $class->getMethod( 'doGuessMimeType' );
+               $method->setAccessible( true );
+               return $method->invokeArgs( $this->mimeAnalyzer, $parameters );
+       }
+
+       /**
+        * @dataProvider providerImproveTypeFromExtension
+        * @param string $ext File extension (no leading dot)
+        * @param string $oldMime Initially detected MIME
+        * @param string $expectedMime MIME type after taking extension into account
+        */
+       function testImproveTypeFromExtension( $ext, $oldMime, $expectedMime ) {
+               $actualMime = $this->mimeAnalyzer->improveTypeFromExtension( $oldMime, $ext );
+               $this->assertEquals( $expectedMime, $actualMime );
+       }
+
+       function providerImproveTypeFromExtension() {
+               return [
+                       [ 'gif', 'image/gif', 'image/gif' ],
+                       [ 'gif', 'unknown/unknown', 'unknown/unknown' ],
+                       [ 'wrl', 'unknown/unknown', 'model/vrml' ],
+                       [ 'txt', 'text/plain', 'text/plain' ],
+                       [ 'csv', 'text/plain', 'text/csv' ],
+                       [ 'tsv', 'text/plain', 'text/tab-separated-values' ],
+                       [ 'js', 'text/javascript', 'application/javascript' ],
+                       [ 'js', 'application/x-javascript', 'application/javascript' ],
+                       [ 'json', 'text/plain', 'application/json' ],
+                       [ 'foo', 'application/x-opc+zip', 'application/zip' ],
+                       [ 'docx', 'application/x-opc+zip',
+                               'application/vnd.openxmlformats-officedocument.wordprocessingml.document' ],
+                       [ 'djvu', 'image/x-djvu', 'image/vnd.djvu' ],
+                       [ 'wav', 'audio/wav', 'audio/wav' ],
+               ];
+       }
+
+       /**
+        * Test to make sure that encoder=ffmpeg2theora doesn't trigger
+        * MEDIATYPE_VIDEO (T65584)
+        */
+       function testOggRecognize() {
+               $oggFile = __DIR__ . '/../../../../data/media/say-test.ogg';
+               $actualType = $this->mimeAnalyzer->getMediaType( $oggFile, 'application/ogg' );
+               $this->assertEquals( MEDIATYPE_AUDIO, $actualType );
+       }
+
+       /**
+        * Test to make sure that Opus audio files don't trigger
+        * MEDIATYPE_MULTIMEDIA (bug T151352)
+        */
+       function testOpusRecognize() {
+               $oggFile = __DIR__ . '/../../../../data/media/say-test.opus';
+               $actualType = $this->mimeAnalyzer->getMediaType( $oggFile, 'application/ogg' );
+               $this->assertEquals( MEDIATYPE_AUDIO, $actualType );
+       }
+
+       /**
+        * Test to make sure that mp3 files are detected as audio type
+        */
+       function testMP3AsAudio() {
+               $file = __DIR__ . '/../../../../data/media/say-test-with-id3.mp3';
+               $actualType = $this->mimeAnalyzer->getMediaType( $file );
+               $this->assertEquals( MEDIATYPE_AUDIO, $actualType );
+       }
+
+       /**
+        * Test to make sure that MP3 with id3 tag is recognized
+        */
+       function testMP3WithID3Recognize() {
+               $file = __DIR__ . '/../../../../data/media/say-test-with-id3.mp3';
+               $actualType = $this->doGuessMimeType( [ $file, 'mp3' ] );
+               $this->assertEquals( 'audio/mpeg', $actualType );
+       }
+
+       /**
+        * Test to make sure that MP3 without id3 tag is recognized (MPEG-1 sample rates)
+        */
+       function testMP3NoID3RecognizeMPEG1() {
+               $file = __DIR__ . '/../../../../data/media/say-test-mpeg1.mp3';
+               $actualType = $this->doGuessMimeType( [ $file, 'mp3' ] );
+               $this->assertEquals( 'audio/mpeg', $actualType );
+       }
+
+       /**
+        * Test to make sure that MP3 without id3 tag is recognized (MPEG-2 sample rates)
+        */
+       function testMP3NoID3RecognizeMPEG2() {
+               $file = __DIR__ . '/../../../../data/media/say-test-mpeg2.mp3';
+               $actualType = $this->doGuessMimeType( [ $file, 'mp3' ] );
+               $this->assertEquals( 'audio/mpeg', $actualType );
+       }
+
+       /**
+        * Test to make sure that MP3 without id3 tag is recognized (MPEG-2.5 sample rates)
+        */
+       function testMP3NoID3RecognizeMPEG2_5() {
+               $file = __DIR__ . '/../../../../data/media/say-test-mpeg2.5.mp3';
+               $actualType = $this->doGuessMimeType( [ $file, 'mp3' ] );
+               $this->assertEquals( 'audio/mpeg', $actualType );
+       }
+
+       /**
+        * A ZIP file embedded in the middle of a .doc file is still a Word Document.
+        */
+       function testZipInDoc() {
+               if ( php_uname( 's' ) === 'Darwin' ) {
+                       $this->markTestSkipped(
+                               'T225019: Disable this test on macOS for now due to byte-order issues'
+                       );
+               }
+
+               $file = __DIR__ . '/../../../../data/media/zip-in-doc.doc';
+               $actualType = $this->doGuessMimeType( [ $file, 'doc' ] );
+               $this->assertEquals( 'application/msword', $actualType );
+       }
+}
diff --git a/tests/phpunit/unit/includes/libs/objectcache/CachedBagOStuffTest.php b/tests/phpunit/unit/includes/libs/objectcache/CachedBagOStuffTest.php
new file mode 100644 (file)
index 0000000..f953319
--- /dev/null
@@ -0,0 +1,159 @@
+<?php
+
+use Wikimedia\TestingAccessWrapper;
+
+/**
+ * @group BagOStuff
+ */
+class CachedBagOStuffTest extends PHPUnit\Framework\TestCase {
+
+       use MediaWikiCoversValidator;
+
+       /**
+        * @covers CachedBagOStuff::__construct
+        * @covers CachedBagOStuff::get
+        */
+       public function testGetFromBackend() {
+               $backend = new HashBagOStuff;
+               $cache = new CachedBagOStuff( $backend );
+
+               $backend->set( 'foo', 'bar' );
+               $this->assertEquals( 'bar', $cache->get( 'foo' ) );
+
+               $backend->set( 'foo', 'baz' );
+               $this->assertEquals( 'bar', $cache->get( 'foo' ), 'cached' );
+       }
+
+       /**
+        * @covers CachedBagOStuff::set
+        * @covers CachedBagOStuff::delete
+        */
+       public function testSetAndDelete() {
+               $backend = new HashBagOStuff;
+               $cache = new CachedBagOStuff( $backend );
+
+               for ( $i = 0; $i < 10; $i++ ) {
+                       $cache->set( "key$i", 1 );
+                       $this->assertEquals( 1, $cache->get( "key$i" ) );
+                       $this->assertEquals( 1, $backend->get( "key$i" ) );
+
+                       $cache->delete( "key$i" );
+                       $this->assertEquals( false, $cache->get( "key$i" ) );
+                       $this->assertEquals( false, $backend->get( "key$i" ) );
+               }
+       }
+
+       /**
+        * @covers CachedBagOStuff::set
+        * @covers CachedBagOStuff::delete
+        */
+       public function testWriteCacheOnly() {
+               $backend = new HashBagOStuff;
+               $cache = new CachedBagOStuff( $backend );
+
+               $cache->set( 'foo', 'bar', 0, CachedBagOStuff::WRITE_CACHE_ONLY );
+               $this->assertEquals( 'bar', $cache->get( 'foo' ) );
+               $this->assertFalse( $backend->get( 'foo' ) );
+
+               $cache->set( 'foo', 'old' );
+               $this->assertEquals( 'old', $cache->get( 'foo' ) );
+               $this->assertEquals( 'old', $backend->get( 'foo' ) );
+
+               $cache->set( 'foo', 'new', 0, CachedBagOStuff::WRITE_CACHE_ONLY );
+               $this->assertEquals( 'new', $cache->get( 'foo' ) );
+               $this->assertEquals( 'old', $backend->get( 'foo' ) );
+
+               $cache->delete( 'foo', CachedBagOStuff::WRITE_CACHE_ONLY );
+               $this->assertEquals( 'old', $cache->get( 'foo' ) ); // Reloaded from backend
+       }
+
+       /**
+        * @covers CachedBagOStuff::get
+        */
+       public function testCacheBackendMisses() {
+               $backend = new HashBagOStuff;
+               $cache = new CachedBagOStuff( $backend );
+
+               // First hit primes the cache with miss from the backend
+               $this->assertEquals( false, $cache->get( 'foo' ) );
+
+               // Change the value in the backend
+               $backend->set( 'foo', true );
+
+               // Second hit returns the cached miss
+               $this->assertEquals( false, $cache->get( 'foo' ) );
+
+               // But a fresh value is read from the backend
+               $backend->set( 'bar', true );
+               $this->assertEquals( true, $cache->get( 'bar' ) );
+       }
+
+       /**
+        * @covers CachedBagOStuff::setDebug
+        */
+       public function testSetDebug() {
+               $backend = new HashBagOStuff();
+               $cache = new CachedBagOStuff( $backend );
+               // Access private property 'debugMode'
+               $backend = TestingAccessWrapper::newFromObject( $backend );
+               $cache = TestingAccessWrapper::newFromObject( $cache );
+               $this->assertFalse( $backend->debugMode );
+               $this->assertFalse( $cache->debugMode );
+
+               $cache->setDebug( true );
+               // Should have set both
+               $this->assertTrue( $backend->debugMode, 'sets backend' );
+               $this->assertTrue( $cache->debugMode, 'sets self' );
+       }
+
+       /**
+        * @covers CachedBagOStuff::deleteObjectsExpiringBefore
+        */
+       public function testExpire() {
+               $backend = $this->getMockBuilder( HashBagOStuff::class )
+                       ->setMethods( [ 'deleteObjectsExpiringBefore' ] )
+                       ->getMock();
+               $backend->expects( $this->once() )
+                       ->method( 'deleteObjectsExpiringBefore' )
+                       ->willReturn( false );
+
+               $cache = new CachedBagOStuff( $backend );
+               $cache->deleteObjectsExpiringBefore( '20110401000000' );
+       }
+
+       /**
+        * @covers CachedBagOStuff::makeKey
+        */
+       public function testMakeKey() {
+               $backend = $this->getMockBuilder( HashBagOStuff::class )
+                       ->setMethods( [ 'makeKey' ] )
+                       ->getMock();
+               $backend->method( 'makeKey' )
+                       ->willReturn( 'special/logic' );
+
+               // CachedBagOStuff wraps any backend with a process cache
+               // using HashBagOStuff. Hash has no special key limitations,
+               // but backends often do. Make sure it uses the backend's
+               // makeKey() logic, not the one inherited from HashBagOStuff
+               $cache = new CachedBagOStuff( $backend );
+
+               $this->assertEquals( 'special/logic', $backend->makeKey( 'special', 'logic' ) );
+               $this->assertEquals( 'special/logic', $cache->makeKey( 'special', 'logic' ) );
+       }
+
+       /**
+        * @covers CachedBagOStuff::makeGlobalKey
+        */
+       public function testMakeGlobalKey() {
+               $backend = $this->getMockBuilder( HashBagOStuff::class )
+                       ->setMethods( [ 'makeGlobalKey' ] )
+                       ->getMock();
+               $backend->method( 'makeGlobalKey' )
+                       ->willReturn( 'special/logic' );
+
+               $cache = new CachedBagOStuff( $backend );
+
+               $this->assertEquals( 'special/logic', $backend->makeGlobalKey( 'special', 'logic' ) );
+               $this->assertEquals( 'special/logic', $cache->makeGlobalKey( 'special', 'logic' ) );
+       }
+}
diff --git a/tests/phpunit/unit/includes/libs/objectcache/HashBagOStuffTest.php b/tests/phpunit/unit/includes/libs/objectcache/HashBagOStuffTest.php
new file mode 100644 (file)
index 0000000..332e23b
--- /dev/null
@@ -0,0 +1,163 @@
+<?php
+
+use Wikimedia\TestingAccessWrapper;
+
+/**
+ * @group BagOStuff
+ */
+class HashBagOStuffTest extends PHPUnit\Framework\TestCase {
+
+       use MediaWikiCoversValidator;
+
+       /**
+        * @covers HashBagOStuff::__construct
+        */
+       public function testConstruct() {
+               $this->assertInstanceOf(
+                       HashBagOStuff::class,
+                       new HashBagOStuff()
+               );
+       }
+
+       /**
+        * @covers HashBagOStuff::__construct
+        * @expectedException InvalidArgumentException
+        */
+       public function testConstructBadZero() {
+               $cache = new HashBagOStuff( [ 'maxKeys' => 0 ] );
+       }
+
+       /**
+        * @covers HashBagOStuff::__construct
+        * @expectedException InvalidArgumentException
+        */
+       public function testConstructBadNeg() {
+               $cache = new HashBagOStuff( [ 'maxKeys' => -1 ] );
+       }
+
+       /**
+        * @covers HashBagOStuff::__construct
+        * @expectedException InvalidArgumentException
+        */
+       public function testConstructBadType() {
+               $cache = new HashBagOStuff( [ 'maxKeys' => 'x' ] );
+       }
+
+       /**
+        * @covers HashBagOStuff::delete
+        */
+       public function testDelete() {
+               $cache = new HashBagOStuff();
+               for ( $i = 0; $i < 10; $i++ ) {
+                       $cache->set( "key$i", 1 );
+                       $this->assertEquals( 1, $cache->get( "key$i" ) );
+                       $cache->delete( "key$i" );
+                       $this->assertEquals( false, $cache->get( "key$i" ) );
+               }
+       }
+
+       /**
+        * @covers HashBagOStuff::clear
+        */
+       public function testClear() {
+               $cache = new HashBagOStuff();
+               for ( $i = 0; $i < 10; $i++ ) {
+                       $cache->set( "key$i", 1 );
+                       $this->assertEquals( 1, $cache->get( "key$i" ) );
+               }
+               $cache->clear();
+               for ( $i = 0; $i < 10; $i++ ) {
+                       $this->assertEquals( false, $cache->get( "key$i" ) );
+               }
+       }
+
+       /**
+        * @covers HashBagOStuff::doGet
+        * @covers HashBagOStuff::expire
+        */
+       public function testExpire() {
+               $cache = new HashBagOStuff();
+               $cacheInternal = TestingAccessWrapper::newFromObject( $cache );
+               $cache->set( 'foo', 1 );
+               $cache->set( 'bar', 1, 10 );
+               $cache->set( 'baz', 1, -10 );
+
+               $this->assertEquals( 0, $cacheInternal->bag['foo'][$cache::KEY_EXP], 'Indefinite' );
+               // 2 seconds tolerance
+               $this->assertEquals( time() + 10, $cacheInternal->bag['bar'][$cache::KEY_EXP], 'Future', 2 );
+               $this->assertEquals( time() - 10, $cacheInternal->bag['baz'][$cache::KEY_EXP], 'Past', 2 );
+
+               $this->assertEquals( 1, $cache->get( 'bar' ), 'Key not expired' );
+               $this->assertEquals( false, $cache->get( 'baz' ), 'Key expired' );
+       }
+
+       /**
+        * Ensure maxKeys eviction prefers keeping new keys.
+        *
+        * @covers HashBagOStuff::set
+        */
+       public function testEvictionAdd() {
+               $cache = new HashBagOStuff( [ 'maxKeys' => 10 ] );
+               for ( $i = 0; $i < 10; $i++ ) {
+                       $cache->set( "key$i", 1 );
+                       $this->assertEquals( 1, $cache->get( "key$i" ) );
+               }
+               for ( $i = 10; $i < 20; $i++ ) {
+                       $cache->set( "key$i", 1 );
+                       $this->assertEquals( 1, $cache->get( "key$i" ) );
+                       $this->assertEquals( false, $cache->get( "key" . ( $i - 10 ) ) );
+               }
+       }
+
+       /**
+        * Ensure maxKeys eviction prefers recently set keys
+        * even if the keys pre-exist.
+        *
+        * @covers HashBagOStuff::set
+        */
+       public function testEvictionSet() {
+               $cache = new HashBagOStuff( [ 'maxKeys' => 3 ] );
+
+               foreach ( [ 'foo', 'bar', 'baz' ] as $key ) {
+                       $cache->set( $key, 1 );
+               }
+
+               // Set existing key
+               $cache->set( 'foo', 1 );
+
+               // Add a 4th key (beyond the allowed maximum)
+               $cache->set( 'quux', 1 );
+
+               // Foo's life should have been extended over Bar
+               foreach ( [ 'foo', 'baz', 'quux' ] as $key ) {
+                       $this->assertEquals( 1, $cache->get( $key ), "Kept $key" );
+               }
+               $this->assertEquals( false, $cache->get( 'bar' ), 'Evicted bar' );
+       }
+
+       /**
+        * Ensure maxKeys eviction prefers recently retrieved keys (LRU).
+        *
+        * @covers HashBagOStuff::doGet
+        * @covers HashBagOStuff::hasKey
+        */
+       public function testEvictionGet() {
+               $cache = new HashBagOStuff( [ 'maxKeys' => 3 ] );
+
+               foreach ( [ 'foo', 'bar', 'baz' ] as $key ) {
+                       $cache->set( $key, 1 );
+               }
+
+               // Get existing key
+               $cache->get( 'foo', 1 );
+
+               // Add a 4th key (beyond the allowed maximum)
+               $cache->set( 'quux', 1 );
+
+               // Foo's life should have been extended over Bar
+               foreach ( [ 'foo', 'baz', 'quux' ] as $key ) {
+                       $this->assertEquals( 1, $cache->get( $key ), "Kept $key" );
+               }
+               $this->assertEquals( false, $cache->get( 'bar' ), 'Evicted bar' );
+       }
+}
diff --git a/tests/phpunit/unit/includes/libs/objectcache/ReplicatedBagOStuffTest.php b/tests/phpunit/unit/includes/libs/objectcache/ReplicatedBagOStuffTest.php
new file mode 100644 (file)
index 0000000..64d282f
--- /dev/null
@@ -0,0 +1,62 @@
+<?php
+
+class ReplicatedBagOStuffTest extends \MediaWikiUnitTestCase {
+       /** @var HashBagOStuff */
+       private $writeCache;
+       /** @var HashBagOStuff */
+       private $readCache;
+       /** @var ReplicatedBagOStuff */
+       private $cache;
+
+       protected function setUp() {
+               parent::setUp();
+
+               $this->writeCache = new HashBagOStuff();
+               $this->readCache = new HashBagOStuff();
+               $this->cache = new ReplicatedBagOStuff( [
+                       'writeFactory' => $this->writeCache,
+                       'readFactory' => $this->readCache,
+               ] );
+       }
+
+       /**
+        * @covers ReplicatedBagOStuff::set
+        */
+       public function testSet() {
+               $key = 'a key';
+               $value = 'a value';
+               $this->cache->set( $key, $value );
+
+               // Write to master.
+               $this->assertEquals( $value, $this->writeCache->get( $key ) );
+               // Don't write to replica. Replication is deferred to backend.
+               $this->assertFalse( $this->readCache->get( $key ) );
+       }
+
+       /**
+        * @covers ReplicatedBagOStuff::get
+        */
+       public function testGet() {
+               $key = 'a key';
+
+               $write = 'one value';
+               $this->writeCache->set( $key, $write );
+               $read = 'another value';
+               $this->readCache->set( $key, $read );
+
+               // Read from replica.
+               $this->assertEquals( $read, $this->cache->get( $key ) );
+       }
+
+       /**
+        * @covers ReplicatedBagOStuff::get
+        */
+       public function testGetAbsent() {
+               $key = 'a key';
+               $value = 'a value';
+               $this->writeCache->set( $key, $value );
+
+               // Don't read from master. No failover if value is absent.
+               $this->assertFalse( $this->cache->get( $key ) );
+       }
+}
diff --git a/tests/phpunit/unit/includes/libs/objectcache/WANObjectCacheTest.php b/tests/phpunit/unit/includes/libs/objectcache/WANObjectCacheTest.php
new file mode 100644 (file)
index 0000000..017d745
--- /dev/null
@@ -0,0 +1,1867 @@
+<?php
+
+use Wikimedia\TestingAccessWrapper;
+
+/**
+ * @covers WANObjectCache::wrap
+ * @covers WANObjectCache::unwrap
+ * @covers WANObjectCache::worthRefreshExpiring
+ * @covers WANObjectCache::worthRefreshPopular
+ * @covers WANObjectCache::isValid
+ * @covers WANObjectCache::getWarmupKeyMisses
+ * @covers WANObjectCache::prefixCacheKeys
+ * @covers WANObjectCache::getProcessCache
+ * @covers WANObjectCache::getNonProcessCachedKeys
+ * @covers WANObjectCache::getRawKeysForWarmup
+ * @covers WANObjectCache::getInterimValue
+ * @covers WANObjectCache::setInterimValue
+ */
+class WANObjectCacheTest extends PHPUnit\Framework\TestCase {
+
+       use MediaWikiCoversValidator;
+       use PHPUnit4And6Compat;
+
+       /** @var WANObjectCache */
+       private $cache;
+       /** @var BagOStuff */
+       private $internalCache;
+
+       protected function setUp() {
+               parent::setUp();
+
+               $this->cache = new WANObjectCache( [
+                       'cache' => new HashBagOStuff()
+               ] );
+
+               $wanCache = TestingAccessWrapper::newFromObject( $this->cache );
+               /** @noinspection PhpUndefinedFieldInspection */
+               $this->internalCache = $wanCache->cache;
+       }
+
+       /**
+        * @dataProvider provideSetAndGet
+        * @covers WANObjectCache::set()
+        * @covers WANObjectCache::get()
+        * @covers WANObjectCache::makeKey()
+        * @param mixed $value
+        * @param int $ttl
+        */
+       public function testSetAndGet( $value, $ttl ) {
+               $curTTL = null;
+               $asOf = null;
+               $key = $this->cache->makeKey( 'x', wfRandomString() );
+
+               $this->cache->get( $key, $curTTL, [], $asOf );
+               $this->assertNull( $curTTL, "Current TTL is null" );
+               $this->assertNull( $asOf, "Current as-of-time is infinite" );
+
+               $t = microtime( true );
+               $this->cache->set( $key, $value, $ttl );
+
+               $this->assertEquals( $value, $this->cache->get( $key, $curTTL, [], $asOf ) );
+               if ( is_infinite( $ttl ) || $ttl == 0 ) {
+                       $this->assertTrue( is_infinite( $curTTL ), "Current TTL is infinite" );
+               } else {
+                       $this->assertGreaterThan( 0, $curTTL, "Current TTL > 0" );
+                       $this->assertLessThanOrEqual( $ttl, $curTTL, "Current TTL < nominal TTL" );
+               }
+               $this->assertGreaterThanOrEqual( $t - 1, $asOf, "As-of-time in range of set() time" );
+               $this->assertLessThanOrEqual( $t + 1, $asOf, "As-of-time in range of set() time" );
+       }
+
+       public static function provideSetAndGet() {
+               return [
+                       [ 14141, 3 ],
+                       [ 3535.666, 3 ],
+                       [ [], 3 ],
+                       [ null, 3 ],
+                       [ '0', 3 ],
+                       [ (object)[ 'meow' ], 3 ],
+                       [ INF, 3 ],
+                       [ '', 3 ],
+                       [ 'pizzacat', INF ],
+               ];
+       }
+
+       /**
+        * @covers WANObjectCache::get()
+        * @covers WANObjectCache::makeGlobalKey()
+        */
+       public function testGetNotExists() {
+               $key = $this->cache->makeGlobalKey( 'y', wfRandomString(), 'p' );
+               $curTTL = null;
+               $value = $this->cache->get( $key, $curTTL );
+
+               $this->assertFalse( $value, "Non-existing key has false value" );
+               $this->assertNull( $curTTL, "Non-existing key has null current TTL" );
+       }
+
+       /**
+        * @covers WANObjectCache::set()
+        */
+       public function testSetOver() {
+               $key = wfRandomString();
+               for ( $i = 0; $i < 3; ++$i ) {
+                       $value = wfRandomString();
+                       $this->cache->set( $key, $value, 3 );
+
+                       $this->assertEquals( $this->cache->get( $key ), $value );
+               }
+       }
+
+       /**
+        * @covers WANObjectCache::set()
+        */
+       public function testStaleSet() {
+               $key = wfRandomString();
+               $value = wfRandomString();
+               $this->cache->set( $key, $value, 3, [ 'since' => microtime( true ) - 30 ] );
+
+               $this->assertFalse( $this->cache->get( $key ), "Stale set() value ignored" );
+       }
+
+       public function testProcessCache() {
+               $mockWallClock = 1549343530.2053;
+               $this->cache->setMockTime( $mockWallClock );
+
+               $hit = 0;
+               $callback = function () use ( &$hit ) {
+                       ++$hit;
+                       return 42;
+               };
+               $keys = [ wfRandomString(), wfRandomString(), wfRandomString() ];
+               $groups = [ 'thiscache:1', 'thatcache:1', 'somecache:1' ];
+
+               foreach ( $keys as $i => $key ) {
+                       $this->cache->getWithSetCallback(
+                               $key, 100, $callback, [ 'pcTTL' => 5, 'pcGroup' => $groups[$i] ] );
+               }
+               $this->assertEquals( 3, $hit );
+
+               foreach ( $keys as $i => $key ) {
+                       $this->cache->getWithSetCallback(
+                               $key, 100, $callback, [ 'pcTTL' => 5, 'pcGroup' => $groups[$i] ] );
+               }
+               $this->assertEquals( 3, $hit, "Values cached" );
+
+               foreach ( $keys as $i => $key ) {
+                       $this->cache->getWithSetCallback(
+                               "$key-2", 100, $callback, [ 'pcTTL' => 5, 'pcGroup' => $groups[$i] ] );
+               }
+               $this->assertEquals( 6, $hit );
+
+               foreach ( $keys as $i => $key ) {
+                       $this->cache->getWithSetCallback(
+                               "$key-2", 100, $callback, [ 'pcTTL' => 5, 'pcGroup' => $groups[$i] ] );
+               }
+               $this->assertEquals( 6, $hit, "New values cached" );
+
+               foreach ( $keys as $i => $key ) {
+                       // Should evict from process cache
+                       $this->cache->delete( $key );
+                       $mockWallClock += 0.001; // cached values will be newer than tombstone
+                       // Get into cache (specific process cache group)
+                       $this->cache->getWithSetCallback(
+                               $key, 100, $callback, [ 'pcTTL' => 5, 'pcGroup' => $groups[$i] ] );
+               }
+               $this->assertEquals( 9, $hit, "Values evicted by delete()" );
+
+               // Get into cache (default process cache group)
+               $key = reset( $keys );
+               $this->cache->getWithSetCallback( $key, 100, $callback, [ 'pcTTL' => 5 ] );
+               $this->assertEquals( 9, $hit, "Value recently interim-cached" );
+
+               $mockWallClock += 0.2; // interim key not brand new
+               $this->cache->clearProcessCache();
+               $this->cache->getWithSetCallback( $key, 100, $callback, [ 'pcTTL' => 5 ] );
+               $this->assertEquals( 10, $hit, "Value calculated (interim key not recent and reset)" );
+               $this->cache->getWithSetCallback( $key, 100, $callback, [ 'pcTTL' => 5 ] );
+               $this->assertEquals( 10, $hit, "Value process cached" );
+
+               $mockWallClock += 0.2; // interim key not brand new
+               $outerCallback = function () use ( &$callback, $key ) {
+                       $v = $this->cache->getWithSetCallback( $key, 100, $callback, [ 'pcTTL' => 5 ] );
+
+                       return 43 + $v;
+               };
+               // Outer key misses and refuses inner key process cache value
+               $this->cache->getWithSetCallback( "$key-miss-outer", 100, $outerCallback );
+               $this->assertEquals( 11, $hit, "Nested callback value process cache skipped" );
+       }
+
+       /**
+        * @dataProvider getWithSetCallback_provider
+        * @covers WANObjectCache::getWithSetCallback()
+        * @covers WANObjectCache::doGetWithSetCallback()
+        * @param array $extOpts
+        * @param bool $versioned
+        */
+       public function testGetWithSetCallback( array $extOpts, $versioned ) {
+               $cache = $this->cache;
+
+               $key = wfRandomString();
+               $value = wfRandomString();
+               $cKey1 = wfRandomString();
+               $cKey2 = wfRandomString();
+
+               $priorValue = null;
+               $priorAsOf = null;
+               $wasSet = 0;
+               $func = function ( $old, &$ttl, &$opts, $asOf )
+               use ( &$wasSet, &$priorValue, &$priorAsOf, $value ) {
+                       ++$wasSet;
+                       $priorValue = $old;
+                       $priorAsOf = $asOf;
+                       $ttl = 20; // override with another value
+                       return $value;
+               };
+
+               $mockWallClock = 1549343530.2053;
+               $priorTime = $mockWallClock; // reference time
+               $cache->setMockTime( $mockWallClock );
+
+               $wasSet = 0;
+               $v = $cache->getWithSetCallback( $key, 30, $func, [ 'lockTSE' => 5 ] + $extOpts );
+               $this->assertEquals( $value, $v, "Value returned" );
+               $this->assertEquals( 1, $wasSet, "Value regenerated" );
+               $this->assertFalse( $priorValue, "No prior value" );
+               $this->assertNull( $priorAsOf, "No prior value" );
+
+               $curTTL = null;
+               $cache->get( $key, $curTTL );
+               $this->assertLessThanOrEqual( 20, $curTTL, 'Current TTL between 19-20 (overriden)' );
+               $this->assertGreaterThanOrEqual( 19, $curTTL, 'Current TTL between 19-20 (overriden)' );
+
+               $wasSet = 0;
+               $v = $cache->getWithSetCallback(
+                       $key, 30, $func, [ 'lowTTL' => 0, 'lockTSE' => 5 ] + $extOpts );
+               $this->assertEquals( $value, $v, "Value returned" );
+               $this->assertEquals( 0, $wasSet, "Value not regenerated" );
+
+               $mockWallClock += 1;
+
+               $wasSet = 0;
+               $v = $cache->getWithSetCallback(
+                       $key, 30, $func, [ 'checkKeys' => [ $cKey1, $cKey2 ] ] + $extOpts
+               );
+               $this->assertEquals( $value, $v, "Value returned" );
+               $this->assertEquals( 1, $wasSet, "Value regenerated due to check keys" );
+               $this->assertEquals( $value, $priorValue, "Has prior value" );
+               $this->assertInternalType( 'float', $priorAsOf, "Has prior value" );
+               $t1 = $cache->getCheckKeyTime( $cKey1 );
+               $this->assertGreaterThanOrEqual( $priorTime, $t1, 'Check keys generated on miss' );
+               $t2 = $cache->getCheckKeyTime( $cKey2 );
+               $this->assertGreaterThanOrEqual( $priorTime, $t2, 'Check keys generated on miss' );
+
+               $mockWallClock += 0.2; // interim key is not brand new and check keys have past values
+               $priorTime = $mockWallClock; // reference time
+               $wasSet = 0;
+               $v = $cache->getWithSetCallback(
+                       $key, 30, $func, [ 'checkKeys' => [ $cKey1, $cKey2 ] ] + $extOpts
+               );
+               $this->assertEquals( $value, $v, "Value returned" );
+               $this->assertEquals( 1, $wasSet, "Value regenerated due to still-recent check keys" );
+               $t1 = $cache->getCheckKeyTime( $cKey1 );
+               $this->assertLessThanOrEqual( $priorTime, $t1, 'Check keys did not change again' );
+               $t2 = $cache->getCheckKeyTime( $cKey2 );
+               $this->assertLessThanOrEqual( $priorTime, $t2, 'Check keys did not change again' );
+
+               $curTTL = null;
+               $v = $cache->get( $key, $curTTL, [ $cKey1, $cKey2 ] );
+               if ( $versioned ) {
+                       $this->assertEquals( $value, $v[$cache::VFLD_DATA], "Value returned" );
+               } else {
+                       $this->assertEquals( $value, $v, "Value returned" );
+               }
+               $this->assertLessThanOrEqual( 0, $curTTL, "Value has current TTL < 0 due to check keys" );
+
+               $wasSet = 0;
+               $key = wfRandomString();
+               $v = $cache->getWithSetCallback( $key, 30, $func, [ 'pcTTL' => 5 ] + $extOpts );
+               $this->assertEquals( $value, $v, "Value returned" );
+               $cache->delete( $key );
+               $v = $cache->getWithSetCallback( $key, 30, $func, [ 'pcTTL' => 5 ] + $extOpts );
+               $this->assertEquals( $value, $v, "Value still returned after deleted" );
+               $this->assertEquals( 1, $wasSet, "Value process cached while deleted" );
+
+               $oldValReceived = -1;
+               $oldAsOfReceived = -1;
+               $checkFunc = function ( $oldVal, &$ttl, array $setOpts, $oldAsOf )
+               use ( &$oldValReceived, &$oldAsOfReceived, &$wasSet ) {
+                       ++$wasSet;
+                       $oldValReceived = $oldVal;
+                       $oldAsOfReceived = $oldAsOf;
+
+                       return 'xxx' . $wasSet;
+               };
+
+               $mockWallClock = 1549343530.2053;
+               $priorTime = $mockWallClock; // reference time
+
+               $wasSet = 0;
+               $key = wfRandomString();
+               $v = $cache->getWithSetCallback(
+                       $key, 30, $checkFunc, [ 'staleTTL' => 50 ] + $extOpts );
+               $this->assertEquals( 'xxx1', $v, "Value returned" );
+               $this->assertEquals( false, $oldValReceived, "Callback got no stale value" );
+               $this->assertEquals( null, $oldAsOfReceived, "Callback got no stale value" );
+
+               $mockWallClock += 40;
+               $v = $cache->getWithSetCallback(
+                       $key, 30, $checkFunc, [ 'staleTTL' => 50 ] + $extOpts );
+               $this->assertEquals( 'xxx2', $v, "Value still returned after expired" );
+               $this->assertEquals( 2, $wasSet, "Value recalculated while expired" );
+               $this->assertEquals( 'xxx1', $oldValReceived, "Callback got stale value" );
+               $this->assertNotEquals( null, $oldAsOfReceived, "Callback got stale value" );
+
+               $mockWallClock += 260;
+               $v = $cache->getWithSetCallback(
+                       $key, 30, $checkFunc, [ 'staleTTL' => 50 ] + $extOpts );
+               $this->assertEquals( 'xxx3', $v, "Value still returned after expired" );
+               $this->assertEquals( 3, $wasSet, "Value recalculated while expired" );
+               $this->assertEquals( false, $oldValReceived, "Callback got no stale value" );
+               $this->assertEquals( null, $oldAsOfReceived, "Callback got no stale value" );
+
+               $mockWallClock = ( $priorTime - $cache::HOLDOFF_TTL - 1 );
+               $wasSet = 0;
+               $key = wfRandomString();
+               $checkKey = $cache->makeKey( 'template', 'X' );
+               $cache->touchCheckKey( $checkKey ); // init check key
+               $mockWallClock = $priorTime;
+               $v = $cache->getWithSetCallback(
+                       $key,
+                       $cache::TTL_INDEFINITE,
+                       $checkFunc,
+                       [ 'graceTTL' => $cache::TTL_WEEK, 'checkKeys' => [ $checkKey ] ] + $extOpts
+               );
+               $this->assertEquals( 'xxx1', $v, "Value returned" );
+               $this->assertEquals( 1, $wasSet, "Value computed" );
+               $this->assertEquals( false, $oldValReceived, "Callback got no stale value" );
+               $this->assertEquals( null, $oldAsOfReceived, "Callback got no stale value" );
+
+               $mockWallClock += $cache::TTL_HOUR; // some time passes
+               $v = $cache->getWithSetCallback(
+                       $key,
+                       $cache::TTL_INDEFINITE,
+                       $checkFunc,
+                       [ 'graceTTL' => $cache::TTL_WEEK, 'checkKeys' => [ $checkKey ] ] + $extOpts
+               );
+               $this->assertEquals( 'xxx1', $v, "Cached value returned" );
+               $this->assertEquals( 1, $wasSet, "Cached value returned" );
+
+               $cache->touchCheckKey( $checkKey ); // make key stale
+               $mockWallClock += 0.01; // ~1 week left of grace (barely stale to avoid refreshes)
+
+               $v = $cache->getWithSetCallback(
+                       $key,
+                       $cache::TTL_INDEFINITE,
+                       $checkFunc,
+                       [ 'graceTTL' => $cache::TTL_WEEK, 'checkKeys' => [ $checkKey ] ] + $extOpts
+               );
+               $this->assertEquals( 'xxx1', $v, "Value still returned after expired (in grace)" );
+               $this->assertEquals( 1, $wasSet, "Value still returned after expired (in grace)" );
+
+               // Chance of refresh increase to unity as staleness approaches graceTTL
+               $mockWallClock += $cache::TTL_WEEK; // 8 days of being stale
+               $v = $cache->getWithSetCallback(
+                       $key,
+                       $cache::TTL_INDEFINITE,
+                       $checkFunc,
+                       [ 'graceTTL' => $cache::TTL_WEEK, 'checkKeys' => [ $checkKey ] ] + $extOpts
+               );
+               $this->assertEquals( 'xxx2', $v, "Value was recomputed (past grace)" );
+               $this->assertEquals( 2, $wasSet, "Value was recomputed (past grace)" );
+               $this->assertEquals( 'xxx1', $oldValReceived, "Callback got post-grace stale value" );
+               $this->assertNotEquals( null, $oldAsOfReceived, "Callback got post-grace stale value" );
+       }
+
+       /**
+        * @dataProvider getWithSetCallback_provider
+        * @covers WANObjectCache::getWithSetCallback()
+        * @covers WANObjectCache::doGetWithSetCallback()
+        * @param array $extOpts
+        * @param bool $versioned
+        */
+       function testGetWithSetcallback_touched( array $extOpts, $versioned ) {
+               $cache = $this->cache;
+
+               $mockWallClock = 1549343530.2053;
+               $cache->setMockTime( $mockWallClock );
+
+               $checkFunc = function ( $oldVal, &$ttl, array $setOpts, $oldAsOf )
+               use ( &$wasSet ) {
+                       ++$wasSet;
+
+                       return 'xxx' . $wasSet;
+               };
+
+               $key = wfRandomString();
+               $wasSet = 0;
+               $touched = null;
+               $touchedCallback = function () use ( &$touched ) {
+                       return $touched;
+               };
+               $v = $cache->getWithSetCallback(
+                       $key,
+                       $cache::TTL_INDEFINITE,
+                       $checkFunc,
+                       [ 'touchedCallback' => $touchedCallback ] + $extOpts
+               );
+               $mockWallClock += 60;
+               $v = $cache->getWithSetCallback(
+                       $key,
+                       $cache::TTL_INDEFINITE,
+                       $checkFunc,
+                       [ 'touchedCallback' => $touchedCallback ] + $extOpts
+               );
+               $this->assertEquals( 'xxx1', $v, "Value was computed once" );
+               $this->assertEquals( 1, $wasSet, "Value was computed once" );
+
+               $touched = $mockWallClock - 10;
+               $v = $cache->getWithSetCallback(
+                       $key,
+                       $cache::TTL_INDEFINITE,
+                       $checkFunc,
+                       [ 'touchedCallback' => $touchedCallback ] + $extOpts
+               );
+               $v = $cache->getWithSetCallback(
+                       $key,
+                       $cache::TTL_INDEFINITE,
+                       $checkFunc,
+                       [ 'touchedCallback' => $touchedCallback ] + $extOpts
+               );
+               $this->assertEquals( 'xxx2', $v, "Value was recomputed once" );
+               $this->assertEquals( 2, $wasSet, "Value was recomputed once" );
+       }
+
+       public static function getWithSetCallback_provider() {
+               return [
+                       [ [], false ],
+                       [ [ 'version' => 1 ], true ]
+               ];
+       }
+
+       public function testPreemtiveRefresh() {
+               $value = 'KatCafe';
+               $wasSet = 0;
+               $func = function ( $old, &$ttl, &$opts, $asOf ) use ( &$wasSet, &$value )
+               {
+                       ++$wasSet;
+                       return $value;
+               };
+
+               $cache = new NearExpiringWANObjectCache( [ 'cache' => new HashBagOStuff() ] );
+               $mockWallClock = 1549343530.2053;
+               $cache->setMockTime( $mockWallClock );
+
+               $wasSet = 0;
+               $key = wfRandomString();
+               $opts = [ 'lowTTL' => 30 ];
+               $v = $cache->getWithSetCallback( $key, 20, $func, $opts );
+               $this->assertEquals( $value, $v, "Value returned" );
+               $this->assertEquals( 1, $wasSet, "Value calculated" );
+
+               $mockWallClock += 0.2; // interim key is not brand new
+               $v = $cache->getWithSetCallback( $key, 20, $func, $opts );
+               $this->assertEquals( 2, $wasSet, "Value re-calculated" );
+
+               $wasSet = 0;
+               $key = wfRandomString();
+               $opts = [ 'lowTTL' => 1 ];
+               $v = $cache->getWithSetCallback( $key, 30, $func, $opts );
+               $this->assertEquals( $value, $v, "Value returned" );
+               $this->assertEquals( 1, $wasSet, "Value calculated" );
+               $v = $cache->getWithSetCallback( $key, 30, $func, $opts );
+               $this->assertEquals( 1, $wasSet, "Value cached" );
+
+               $asycList = [];
+               $asyncHandler = function ( $callback ) use ( &$asycList ) {
+                       $asycList[] = $callback;
+               };
+               $cache = new NearExpiringWANObjectCache( [
+                       'cache'        => new HashBagOStuff(),
+                       'asyncHandler' => $asyncHandler
+               ] );
+
+               $mockWallClock = 1549343530.2053;
+               $priorTime = $mockWallClock; // reference time
+               $cache->setMockTime( $mockWallClock );
+
+               $wasSet = 0;
+               $key = wfRandomString();
+               $opts = [ 'lowTTL' => 100 ];
+               $v = $cache->getWithSetCallback( $key, 300, $func, $opts );
+               $this->assertEquals( $value, $v, "Value returned" );
+               $this->assertEquals( 1, $wasSet, "Value calculated" );
+               $v = $cache->getWithSetCallback( $key, 300, $func, $opts );
+               $this->assertEquals( 1, $wasSet, "Cached value used" );
+               $this->assertEquals( $v, $value, "Value cached" );
+
+               $mockWallClock += 250;
+               $v = $cache->getWithSetCallback( $key, 300, $func, $opts );
+               $this->assertEquals( $value, $v, "Value returned" );
+               $this->assertEquals( 1, $wasSet, "Stale value used" );
+               $this->assertEquals( 1, count( $asycList ), "Refresh deferred." );
+               $value = 'NewCatsInTown'; // change callback return value
+               $asycList[0](); // run the refresh callback
+               $asycList = [];
+               $this->assertEquals( 2, $wasSet, "Value calculated at later time" );
+               $this->assertEquals( 0, count( $asycList ), "No deferred refreshes added." );
+               $v = $cache->getWithSetCallback( $key, 300, $func, $opts );
+               $this->assertEquals( $value, $v, "New value stored" );
+
+               $cache = new PopularityRefreshingWANObjectCache( [
+                       'cache'   => new HashBagOStuff()
+               ] );
+
+               $mockWallClock = $priorTime;
+               $cache->setMockTime( $mockWallClock );
+
+               $wasSet = 0;
+               $key = wfRandomString();
+               $opts = [ 'hotTTR' => 900 ];
+               $v = $cache->getWithSetCallback( $key, 60, $func, $opts );
+               $this->assertEquals( $value, $v, "Value returned" );
+               $this->assertEquals( 1, $wasSet, "Value calculated" );
+
+               $mockWallClock += 30;
+
+               $v = $cache->getWithSetCallback( $key, 60, $func, $opts );
+               $this->assertEquals( 1, $wasSet, "Value cached" );
+
+               $mockWallClock = $priorTime;
+               $wasSet = 0;
+               $key = wfRandomString();
+               $opts = [ 'hotTTR' => 10 ];
+               $v = $cache->getWithSetCallback( $key, 60, $func, $opts );
+               $this->assertEquals( $value, $v, "Value returned" );
+               $this->assertEquals( 1, $wasSet, "Value calculated" );
+
+               $mockWallClock += 30;
+
+               $v = $cache->getWithSetCallback( $key, 60, $func, $opts );
+               $this->assertEquals( $value, $v, "Value returned" );
+               $this->assertEquals( 2, $wasSet, "Value re-calculated" );
+       }
+
+       /**
+        * @covers WANObjectCache::getWithSetCallback()
+        * @covers WANObjectCache::doGetWithSetCallback()
+        */
+       public function testGetWithSetCallback_invalidCallback() {
+               $this->setExpectedException( InvalidArgumentException::class );
+               $this->cache->getWithSetCallback( 'key', 30, 'invalid callback' );
+       }
+
+       /**
+        * @dataProvider getMultiWithSetCallback_provider
+        * @covers WANObjectCache::getMultiWithSetCallback
+        * @covers WANObjectCache::makeMultiKeys
+        * @covers WANObjectCache::getMulti
+        * @param array $extOpts
+        * @param bool $versioned
+        */
+       public function testGetMultiWithSetCallback( array $extOpts, $versioned ) {
+               $cache = $this->cache;
+
+               $keyA = wfRandomString();
+               $keyB = wfRandomString();
+               $keyC = wfRandomString();
+               $cKey1 = wfRandomString();
+               $cKey2 = wfRandomString();
+
+               $priorValue = null;
+               $priorAsOf = null;
+               $wasSet = 0;
+               $genFunc = function ( $id, $old, &$ttl, &$opts, $asOf ) use (
+                       &$wasSet, &$priorValue, &$priorAsOf
+               ) {
+                       ++$wasSet;
+                       $priorValue = $old;
+                       $priorAsOf = $asOf;
+                       $ttl = 20; // override with another value
+                       return "@$id$";
+               };
+
+               $mockWallClock = 1549343530.2053;
+               $priorTime = $mockWallClock; // reference time
+               $cache->setMockTime( $mockWallClock );
+
+               $wasSet = 0;
+               $keyedIds = new ArrayIterator( [ $keyA => 3353 ] );
+               $value = "@3353$";
+               $v = $cache->getMultiWithSetCallback(
+                       $keyedIds, 30, $genFunc, [ 'lockTSE' => 5 ] + $extOpts );
+               $this->assertEquals( $value, $v[$keyA], "Value returned" );
+               $this->assertEquals( 1, $wasSet, "Value regenerated" );
+               $this->assertFalse( $priorValue, "No prior value" );
+               $this->assertNull( $priorAsOf, "No prior value" );
+
+               $curTTL = null;
+               $cache->get( $keyA, $curTTL );
+               $this->assertLessThanOrEqual( 20, $curTTL, 'Current TTL between 19-20 (overriden)' );
+               $this->assertGreaterThanOrEqual( 19, $curTTL, 'Current TTL between 19-20 (overriden)' );
+
+               $wasSet = 0;
+               $value = "@efef$";
+               $keyedIds = new ArrayIterator( [ $keyB => 'efef' ] );
+               $v = $cache->getMultiWithSetCallback(
+                       $keyedIds, 30, $genFunc, [ 'lowTTL' => 0, 'lockTSE' => 5, ] + $extOpts );
+               $this->assertEquals( $value, $v[$keyB], "Value returned" );
+               $this->assertEquals( 1, $wasSet, "Value regenerated" );
+               $this->assertEquals( 0, $cache->getWarmupKeyMisses(), "Keys warmed yet in process cache" );
+               $v = $cache->getMultiWithSetCallback(
+                       $keyedIds, 30, $genFunc, [ 'lowTTL' => 0, 'lockTSE' => 5, ] + $extOpts );
+               $this->assertEquals( $value, $v[$keyB], "Value returned" );
+               $this->assertEquals( 1, $wasSet, "Value not regenerated" );
+               $this->assertEquals( 0, $cache->getWarmupKeyMisses(), "Keys warmed in process cache" );
+
+               $mockWallClock += 1;
+
+               $wasSet = 0;
+               $keyedIds = new ArrayIterator( [ $keyB => 'efef' ] );
+               $v = $cache->getMultiWithSetCallback(
+                       $keyedIds, 30, $genFunc, [ 'checkKeys' => [ $cKey1, $cKey2 ] ] + $extOpts
+               );
+               $this->assertEquals( $value, $v[$keyB], "Value returned" );
+               $this->assertEquals( 1, $wasSet, "Value regenerated due to check keys" );
+               $this->assertEquals( $value, $priorValue, "Has prior value" );
+               $this->assertInternalType( 'float', $priorAsOf, "Has prior value" );
+               $t1 = $cache->getCheckKeyTime( $cKey1 );
+               $this->assertGreaterThanOrEqual( $priorTime, $t1, 'Check keys generated on miss' );
+               $t2 = $cache->getCheckKeyTime( $cKey2 );
+               $this->assertGreaterThanOrEqual( $priorTime, $t2, 'Check keys generated on miss' );
+
+               $mockWallClock += 0.01;
+               $priorTime = $mockWallClock;
+               $value = "@43636$";
+               $wasSet = 0;
+               $keyedIds = new ArrayIterator( [ $keyC => 43636 ] );
+               $v = $cache->getMultiWithSetCallback(
+                       $keyedIds, 30, $genFunc, [ 'checkKeys' => [ $cKey1, $cKey2 ] ] + $extOpts
+               );
+               $this->assertEquals( $value, $v[$keyC], "Value returned" );
+               $this->assertEquals( 1, $wasSet, "Value regenerated due to still-recent check keys" );
+               $t1 = $cache->getCheckKeyTime( $cKey1 );
+               $this->assertLessThanOrEqual( $priorTime, $t1, 'Check keys did not change again' );
+               $t2 = $cache->getCheckKeyTime( $cKey2 );
+               $this->assertLessThanOrEqual( $priorTime, $t2, 'Check keys did not change again' );
+
+               $curTTL = null;
+               $v = $cache->get( $keyC, $curTTL, [ $cKey1, $cKey2 ] );
+               if ( $versioned ) {
+                       $this->assertEquals( $value, $v[$cache::VFLD_DATA], "Value returned" );
+               } else {
+                       $this->assertEquals( $value, $v, "Value returned" );
+               }
+               $this->assertLessThanOrEqual( 0, $curTTL, "Value has current TTL < 0 due to check keys" );
+
+               $wasSet = 0;
+               $key = wfRandomString();
+               $keyedIds = new ArrayIterator( [ $key => 242424 ] );
+               $v = $cache->getMultiWithSetCallback(
+                       $keyedIds, 30, $genFunc, [ 'pcTTL' => 5 ] + $extOpts );
+               $this->assertEquals( "@{$keyedIds[$key]}$", $v[$key], "Value returned" );
+               $cache->delete( $key );
+               $keyedIds = new ArrayIterator( [ $key => 242424 ] );
+               $v = $cache->getMultiWithSetCallback(
+                       $keyedIds, 30, $genFunc, [ 'pcTTL' => 5 ] + $extOpts );
+               $this->assertEquals( "@{$keyedIds[$key]}$", $v[$key], "Value still returned after deleted" );
+               $this->assertEquals( 1, $wasSet, "Value process cached while deleted" );
+
+               $calls = 0;
+               $ids = [ 1, 2, 3, 4, 5, 6 ];
+               $keyFunc = function ( $id, WANObjectCache $wanCache ) {
+                       return $wanCache->makeKey( 'test', $id );
+               };
+               $keyedIds = $cache->makeMultiKeys( $ids, $keyFunc );
+               $genFunc = function ( $id, $oldValue, &$ttl, array &$setops ) use ( &$calls ) {
+                       ++$calls;
+
+                       return "val-{$id}";
+               };
+               $values = $cache->getMultiWithSetCallback( $keyedIds, 10, $genFunc );
+
+               $this->assertEquals(
+                       [ "val-1", "val-2", "val-3", "val-4", "val-5", "val-6" ],
+                       array_values( $values ),
+                       "Correct values in correct order"
+               );
+               $this->assertEquals(
+                       array_map( $keyFunc, $ids, array_fill( 0, count( $ids ), $this->cache ) ),
+                       array_keys( $values ),
+                       "Correct keys in correct order"
+               );
+               $this->assertEquals( count( $ids ), $calls );
+
+               $cache->getMultiWithSetCallback( $keyedIds, 10, $genFunc );
+               $this->assertEquals( count( $ids ), $calls, "Values cached" );
+
+               // Mock the BagOStuff to assure only one getMulti() call given process caching
+               $localBag = $this->getMockBuilder( HashBagOStuff::class )
+                       ->setMethods( [ 'getMulti' ] )->getMock();
+               $localBag->expects( $this->exactly( 1 ) )->method( 'getMulti' )->willReturn( [
+                       WANObjectCache::VALUE_KEY_PREFIX . 'k1' => 'val-id1',
+                       WANObjectCache::VALUE_KEY_PREFIX . 'k2' => 'val-id2'
+               ] );
+               $wanCache = new WANObjectCache( [ 'cache' => $localBag ] );
+
+               // Warm the process cache
+               $keyedIds = new ArrayIterator( [ 'k1' => 'id1', 'k2' => 'id2' ] );
+               $this->assertEquals(
+                       [ 'k1' => 'val-id1', 'k2' => 'val-id2' ],
+                       $wanCache->getMultiWithSetCallback( $keyedIds, 10, $genFunc, [ 'pcTTL' => 5 ] )
+               );
+               // Use the process cache
+               $this->assertEquals(
+                       [ 'k1' => 'val-id1', 'k2' => 'val-id2' ],
+                       $wanCache->getMultiWithSetCallback( $keyedIds, 10, $genFunc, [ 'pcTTL' => 5 ] )
+               );
+       }
+
+       public static function getMultiWithSetCallback_provider() {
+               return [
+                       [ [], false ],
+                       [ [ 'version' => 1 ], true ]
+               ];
+       }
+
+       /**
+        * @dataProvider getMultiWithUnionSetCallback_provider
+        * @covers WANObjectCache::getMultiWithUnionSetCallback()
+        * @covers WANObjectCache::makeMultiKeys()
+        * @param array $extOpts
+        * @param bool $versioned
+        */
+       public function testGetMultiWithUnionSetCallback( array $extOpts, $versioned ) {
+               $cache = $this->cache;
+
+               $keyA = wfRandomString();
+               $keyB = wfRandomString();
+               $keyC = wfRandomString();
+               $cKey1 = wfRandomString();
+               $cKey2 = wfRandomString();
+
+               $wasSet = 0;
+               $genFunc = function ( array $ids, array &$ttls, array &$setOpts ) use (
+                       &$wasSet, &$priorValue, &$priorAsOf
+               ) {
+                       $newValues = [];
+                       foreach ( $ids as $id ) {
+                               ++$wasSet;
+                               $newValues[$id] = "@$id$";
+                               $ttls[$id] = 20; // override with another value
+                       }
+
+                       return $newValues;
+               };
+
+               $mockWallClock = 1549343530.2053;
+               $priorTime = $mockWallClock; // reference time
+               $cache->setMockTime( $mockWallClock );
+
+               $wasSet = 0;
+               $keyedIds = new ArrayIterator( [ $keyA => 3353 ] );
+               $value = "@3353$";
+               $v = $cache->getMultiWithUnionSetCallback(
+                       $keyedIds, 30, $genFunc, $extOpts );
+               $this->assertEquals( $value, $v[$keyA], "Value returned" );
+               $this->assertEquals( 1, $wasSet, "Value regenerated" );
+
+               $curTTL = null;
+               $cache->get( $keyA, $curTTL );
+               $this->assertLessThanOrEqual( 20, $curTTL, 'Current TTL between 19-20 (overriden)' );
+               $this->assertGreaterThanOrEqual( 19, $curTTL, 'Current TTL between 19-20 (overriden)' );
+
+               $wasSet = 0;
+               $value = "@efef$";
+               $keyedIds = new ArrayIterator( [ $keyB => 'efef' ] );
+               $v = $cache->getMultiWithUnionSetCallback(
+                       $keyedIds, 30, $genFunc, [ 'lowTTL' => 0 ] + $extOpts );
+               $this->assertEquals( $value, $v[$keyB], "Value returned" );
+               $this->assertEquals( 1, $wasSet, "Value regenerated" );
+               $this->assertEquals( 0, $cache->getWarmupKeyMisses(), "Keys warmed yet in process cache" );
+               $v = $cache->getMultiWithUnionSetCallback(
+                       $keyedIds, 30, $genFunc, [ 'lowTTL' => 0 ] + $extOpts );
+               $this->assertEquals( $value, $v[$keyB], "Value returned" );
+               $this->assertEquals( 1, $wasSet, "Value not regenerated" );
+               $this->assertEquals( 0, $cache->getWarmupKeyMisses(), "Keys warmed in process cache" );
+
+               $mockWallClock += 1;
+
+               $wasSet = 0;
+               $keyedIds = new ArrayIterator( [ $keyB => 'efef' ] );
+               $v = $cache->getMultiWithUnionSetCallback(
+                       $keyedIds, 30, $genFunc, [ 'checkKeys' => [ $cKey1, $cKey2 ] ] + $extOpts
+               );
+               $this->assertEquals( $value, $v[$keyB], "Value returned" );
+               $this->assertEquals( 1, $wasSet, "Value regenerated due to check keys" );
+               $t1 = $cache->getCheckKeyTime( $cKey1 );
+               $this->assertGreaterThanOrEqual( $priorTime, $t1, 'Check keys generated on miss' );
+               $t2 = $cache->getCheckKeyTime( $cKey2 );
+               $this->assertGreaterThanOrEqual( $priorTime, $t2, 'Check keys generated on miss' );
+
+               $mockWallClock += 0.01;
+               $priorTime = $mockWallClock;
+               $value = "@43636$";
+               $wasSet = 0;
+               $keyedIds = new ArrayIterator( [ $keyC => 43636 ] );
+               $v = $cache->getMultiWithUnionSetCallback(
+                       $keyedIds, 30, $genFunc, [ 'checkKeys' => [ $cKey1, $cKey2 ] ] + $extOpts
+               );
+               $this->assertEquals( $value, $v[$keyC], "Value returned" );
+               $this->assertEquals( 1, $wasSet, "Value regenerated due to still-recent check keys" );
+               $t1 = $cache->getCheckKeyTime( $cKey1 );
+               $this->assertLessThanOrEqual( $priorTime, $t1, 'Check keys did not change again' );
+               $t2 = $cache->getCheckKeyTime( $cKey2 );
+               $this->assertLessThanOrEqual( $priorTime, $t2, 'Check keys did not change again' );
+
+               $curTTL = null;
+               $v = $cache->get( $keyC, $curTTL, [ $cKey1, $cKey2 ] );
+               if ( $versioned ) {
+                       $this->assertEquals( $value, $v[$cache::VFLD_DATA], "Value returned" );
+               } else {
+                       $this->assertEquals( $value, $v, "Value returned" );
+               }
+               $this->assertLessThanOrEqual( 0, $curTTL, "Value has current TTL < 0 due to check keys" );
+
+               $wasSet = 0;
+               $key = wfRandomString();
+               $keyedIds = new ArrayIterator( [ $key => 242424 ] );
+               $v = $cache->getMultiWithUnionSetCallback(
+                       $keyedIds, 30, $genFunc, [ 'pcTTL' => 5 ] + $extOpts );
+               $this->assertEquals( "@{$keyedIds[$key]}$", $v[$key], "Value returned" );
+               $cache->delete( $key );
+               $keyedIds = new ArrayIterator( [ $key => 242424 ] );
+               $v = $cache->getMultiWithUnionSetCallback(
+                       $keyedIds, 30, $genFunc, [ 'pcTTL' => 5 ] + $extOpts );
+               $this->assertEquals( "@{$keyedIds[$key]}$", $v[$key], "Value still returned after deleted" );
+               $this->assertEquals( 1, $wasSet, "Value process cached while deleted" );
+
+               $calls = 0;
+               $ids = [ 1, 2, 3, 4, 5, 6 ];
+               $keyFunc = function ( $id, WANObjectCache $wanCache ) {
+                       return $wanCache->makeKey( 'test', $id );
+               };
+               $keyedIds = $cache->makeMultiKeys( $ids, $keyFunc );
+               $genFunc = function ( array $ids, array &$ttls, array &$setOpts ) use ( &$calls ) {
+                       $newValues = [];
+                       foreach ( $ids as $id ) {
+                               ++$calls;
+                               $newValues[$id] = "val-{$id}";
+                       }
+
+                       return $newValues;
+               };
+               $values = $cache->getMultiWithUnionSetCallback( $keyedIds, 10, $genFunc );
+
+               $this->assertEquals(
+                       [ "val-1", "val-2", "val-3", "val-4", "val-5", "val-6" ],
+                       array_values( $values ),
+                       "Correct values in correct order"
+               );
+               $this->assertEquals(
+                       array_map( $keyFunc, $ids, array_fill( 0, count( $ids ), $this->cache ) ),
+                       array_keys( $values ),
+                       "Correct keys in correct order"
+               );
+               $this->assertEquals( count( $ids ), $calls );
+
+               $cache->getMultiWithUnionSetCallback( $keyedIds, 10, $genFunc );
+               $this->assertEquals( count( $ids ), $calls, "Values cached" );
+       }
+
+       public static function getMultiWithUnionSetCallback_provider() {
+               return [
+                       [ [], false ],
+                       [ [ 'version' => 1 ], true ]
+               ];
+       }
+
+       /**
+        * @covers WANObjectCache::getWithSetCallback()
+        * @covers WANObjectCache::doGetWithSetCallback()
+        */
+       public function testLockTSE() {
+               $cache = $this->cache;
+               $key = wfRandomString();
+               $value = wfRandomString();
+
+               $mockWallClock = 1549343530.2053;
+               $cache->setMockTime( $mockWallClock );
+
+               $calls = 0;
+               $func = function () use ( &$calls, $value, $cache, $key ) {
+                       ++$calls;
+                       return $value;
+               };
+
+               $ret = $cache->getWithSetCallback( $key, 30, $func, [ 'lockTSE' => 5 ] );
+               $this->assertEquals( $value, $ret );
+               $this->assertEquals( 1, $calls, 'Value was populated' );
+
+               // Acquire the mutex to verify that getWithSetCallback uses lockTSE properly
+               $this->internalCache->add( $cache::MUTEX_KEY_PREFIX . $key, 1, 0 );
+
+               $checkKeys = [ wfRandomString() ]; // new check keys => force misses
+               $ret = $cache->getWithSetCallback( $key, 30, $func,
+                       [ 'lockTSE' => 5, 'checkKeys' => $checkKeys ] );
+               $this->assertEquals( $value, $ret, 'Old value used' );
+               $this->assertEquals( 1, $calls, 'Callback was not used' );
+
+               $cache->delete( $key );
+               $mockWallClock += 0.001; // cached values will be newer than tombstone
+               $ret = $cache->getWithSetCallback( $key, 30, $func,
+                       [ 'lockTSE' => 5, 'checkKeys' => $checkKeys ] );
+               $this->assertEquals( $value, $ret, 'Callback was used; interim saved' );
+               $this->assertEquals( 2, $calls, 'Callback was used; interim saved' );
+
+               $ret = $cache->getWithSetCallback( $key, 30, $func,
+                       [ 'lockTSE' => 5, 'checkKeys' => $checkKeys ] );
+               $this->assertEquals( $value, $ret, 'Callback was not used; used interim (mutex failed)' );
+               $this->assertEquals( 2, $calls, 'Callback was not used; used interim (mutex failed)' );
+       }
+
+       /**
+        * @covers WANObjectCache::getWithSetCallback()
+        * @covers WANObjectCache::doGetWithSetCallback()
+        * @covers WANObjectCache::set()
+        */
+       public function testLockTSESlow() {
+               $cache = $this->cache;
+               $key = wfRandomString();
+               $key2 = wfRandomString();
+               $value = wfRandomString();
+
+               $mockWallClock = 1549343530.2053;
+               $cache->setMockTime( $mockWallClock );
+
+               $calls = 0;
+               $func = function ( $oldValue, &$ttl, &$setOpts ) use ( &$calls, $value, &$mockWallClock ) {
+                       ++$calls;
+                       $setOpts['since'] = $mockWallClock - 10;
+                       return $value;
+               };
+
+               // Value should be given a low logical TTL due to snapshot lag
+               $curTTL = null;
+               $ret = $cache->getWithSetCallback( $key, 300, $func, [ 'lockTSE' => 5 ] );
+               $this->assertEquals( $value, $ret );
+               $this->assertEquals( $value, $cache->get( $key, $curTTL ), 'Value was populated' );
+               $this->assertEquals( 1, $curTTL, 'Value has reduced logical TTL', 0.01 );
+               $this->assertEquals( 1, $calls, 'Value was generated' );
+
+               $mockWallClock += 2; // low logical TTL expired
+
+               $ret = $cache->getWithSetCallback( $key, 300, $func, [ 'lockTSE' => 5 ] );
+               $this->assertEquals( $value, $ret );
+               $this->assertEquals( 2, $calls, 'Callback used (mutex acquired)' );
+
+               $ret = $cache->getWithSetCallback( $key, 300, $func, [ 'lockTSE' => 5 ] );
+               $this->assertEquals( $value, $ret );
+               $this->assertEquals( 2, $calls, 'Callback was not used (interim value used)' );
+
+               $mockWallClock += 2; // low logical TTL expired
+               // Acquire a lock to verify that getWithSetCallback uses lockTSE properly
+               $this->internalCache->add( $cache::MUTEX_KEY_PREFIX . $key, 1, 0 );
+
+               $ret = $cache->getWithSetCallback( $key, 300, $func, [ 'lockTSE' => 5 ] );
+               $this->assertEquals( $value, $ret );
+               $this->assertEquals( 2, $calls, 'Callback was not used (mutex not acquired)' );
+
+               $mockWallClock += 301; // physical TTL expired
+               // Acquire a lock to verify that getWithSetCallback uses lockTSE properly
+               $this->internalCache->add( $cache::MUTEX_KEY_PREFIX . $key, 1, 0 );
+
+               $ret = $cache->getWithSetCallback( $key, 300, $func, [ 'lockTSE' => 5 ] );
+               $this->assertEquals( $value, $ret );
+               $this->assertEquals( 3, $calls, 'Callback was used (mutex not acquired, not in cache)' );
+
+               $calls = 0;
+               $func2 = function ( $oldValue, &$ttl, &$setOpts ) use ( &$calls, $value ) {
+                       ++$calls;
+                       $setOpts['lag'] = 15;
+                       return $value;
+               };
+
+               // Value should be given a low logical TTL due to replication lag
+               $curTTL = null;
+               $ret = $cache->getWithSetCallback( $key2, 300, $func2, [ 'lockTSE' => 5 ] );
+               $this->assertEquals( $value, $ret );
+               $this->assertEquals( $value, $cache->get( $key2, $curTTL ), 'Value was populated' );
+               $this->assertEquals( 30, $curTTL, 'Value has reduced logical TTL', 0.01 );
+               $this->assertEquals( 1, $calls, 'Value was generated' );
+
+               $ret = $cache->getWithSetCallback( $key2, 300, $func2, [ 'lockTSE' => 5 ] );
+               $this->assertEquals( $value, $ret );
+               $this->assertEquals( 1, $calls, 'Callback was used (not expired)' );
+
+               $mockWallClock += 31;
+
+               $ret = $cache->getWithSetCallback( $key2, 300, $func2, [ 'lockTSE' => 5 ] );
+               $this->assertEquals( $value, $ret );
+               $this->assertEquals( 2, $calls, 'Callback was used (mutex acquired)' );
+       }
+
+       /**
+        * @covers WANObjectCache::getWithSetCallback()
+        * @covers WANObjectCache::doGetWithSetCallback()
+        */
+       public function testBusyValue() {
+               $cache = $this->cache;
+               $key = wfRandomString();
+               $value = wfRandomString();
+               $busyValue = wfRandomString();
+
+               $mockWallClock = 1549343530.2053;
+               $cache->setMockTime( $mockWallClock );
+
+               $calls = 0;
+               $func = function () use ( &$calls, $value, $cache, $key ) {
+                       ++$calls;
+                       return $value;
+               };
+
+               $ret = $cache->getWithSetCallback( $key, 30, $func, [ 'busyValue' => $busyValue ] );
+               $this->assertEquals( $value, $ret );
+               $this->assertEquals( 1, $calls, 'Value was populated' );
+
+               $mockWallClock += 0.2; // interim keys not brand new
+
+               // Acquire a lock to verify that getWithSetCallback uses busyValue properly
+               $this->internalCache->add( $cache::MUTEX_KEY_PREFIX . $key, 1, 0 );
+
+               $checkKeys = [ wfRandomString() ]; // new check keys => force misses
+               $ret = $cache->getWithSetCallback( $key, 30, $func,
+                       [ 'busyValue' => $busyValue, 'checkKeys' => $checkKeys ] );
+               $this->assertEquals( $value, $ret, 'Callback used' );
+               $this->assertEquals( 2, $calls, 'Callback used' );
+
+               $ret = $cache->getWithSetCallback( $key, 30, $func,
+                       [ 'lockTSE' => 30, 'busyValue' => $busyValue, 'checkKeys' => $checkKeys ] );
+               $this->assertEquals( $value, $ret, 'Old value used' );
+               $this->assertEquals( 2, $calls, 'Callback was not used' );
+
+               $cache->delete( $key ); // no value at all anymore and still locked
+               $ret = $cache->getWithSetCallback( $key, 30, $func,
+                       [ 'busyValue' => $busyValue, 'checkKeys' => $checkKeys ] );
+               $this->assertEquals( $busyValue, $ret, 'Callback was not used; used busy value' );
+               $this->assertEquals( 2, $calls, 'Callback was not used; used busy value' );
+
+               $this->internalCache->delete( $cache::MUTEX_KEY_PREFIX . $key );
+               $mockWallClock += 0.001; // cached values will be newer than tombstone
+               $ret = $cache->getWithSetCallback( $key, 30, $func,
+                       [ 'lockTSE' => 30, 'busyValue' => $busyValue, 'checkKeys' => $checkKeys ] );
+               $this->assertEquals( $value, $ret, 'Callback was used; saved interim' );
+               $this->assertEquals( 3, $calls, 'Callback was used; saved interim' );
+
+               $this->internalCache->add( $cache::MUTEX_KEY_PREFIX . $key, 1, 0 );
+               $ret = $cache->getWithSetCallback( $key, 30, $func,
+                       [ 'busyValue' => $busyValue, 'checkKeys' => $checkKeys ] );
+               $this->assertEquals( $value, $ret, 'Callback was not used; used interim' );
+               $this->assertEquals( 3, $calls, 'Callback was not used; used interim' );
+       }
+
+       /**
+        * @covers WANObjectCache::getMulti()
+        */
+       public function testGetMulti() {
+               $cache = $this->cache;
+
+               $value1 = [ 'this' => 'is', 'a' => 'test' ];
+               $value2 = [ 'this' => 'is', 'another' => 'test' ];
+
+               $key1 = wfRandomString();
+               $key2 = wfRandomString();
+               $key3 = wfRandomString();
+
+               $mockWallClock = 1549343530.2053;
+               $priorTime = $mockWallClock; // reference time
+               $cache->setMockTime( $mockWallClock );
+
+               $cache->set( $key1, $value1, 5 );
+               $cache->set( $key2, $value2, 10 );
+
+               $curTTLs = [];
+               $this->assertEquals(
+                       [ $key1 => $value1, $key2 => $value2 ],
+                       $cache->getMulti( [ $key1, $key2, $key3 ], $curTTLs ),
+                       'Result array populated'
+               );
+
+               $this->assertEquals( 2, count( $curTTLs ), "Two current TTLs in array" );
+               $this->assertGreaterThan( 0, $curTTLs[$key1], "Key 1 has current TTL > 0" );
+               $this->assertGreaterThan( 0, $curTTLs[$key2], "Key 2 has current TTL > 0" );
+
+               $cKey1 = wfRandomString();
+               $cKey2 = wfRandomString();
+
+               $mockWallClock += 1;
+
+               $curTTLs = [];
+               $this->assertEquals(
+                       [ $key1 => $value1, $key2 => $value2 ],
+                       $cache->getMulti( [ $key1, $key2, $key3 ], $curTTLs, [ $cKey1, $cKey2 ] ),
+                       "Result array populated even with new check keys"
+               );
+               $t1 = $cache->getCheckKeyTime( $cKey1 );
+               $this->assertGreaterThanOrEqual( $priorTime, $t1, 'Check key 1 generated on miss' );
+               $t2 = $cache->getCheckKeyTime( $cKey2 );
+               $this->assertGreaterThanOrEqual( $priorTime, $t2, 'Check key 2 generated on miss' );
+               $this->assertEquals( 2, count( $curTTLs ), "Current TTLs array set" );
+               $this->assertLessThanOrEqual( 0, $curTTLs[$key1], 'Key 1 has current TTL <= 0' );
+               $this->assertLessThanOrEqual( 0, $curTTLs[$key2], 'Key 2 has current TTL <= 0' );
+
+               $mockWallClock += 1;
+
+               $curTTLs = [];
+               $this->assertEquals(
+                       [ $key1 => $value1, $key2 => $value2 ],
+                       $cache->getMulti( [ $key1, $key2, $key3 ], $curTTLs, [ $cKey1, $cKey2 ] ),
+                       "Result array still populated even with new check keys"
+               );
+               $this->assertEquals( 2, count( $curTTLs ), "Current TTLs still array set" );
+               $this->assertLessThan( 0, $curTTLs[$key1], 'Key 1 has negative current TTL' );
+               $this->assertLessThan( 0, $curTTLs[$key2], 'Key 2 has negative current TTL' );
+       }
+
+       /**
+        * @covers WANObjectCache::getMulti()
+        * @covers WANObjectCache::processCheckKeys()
+        */
+       public function testGetMultiCheckKeys() {
+               $cache = $this->cache;
+
+               $checkAll = wfRandomString();
+               $check1 = wfRandomString();
+               $check2 = wfRandomString();
+               $check3 = wfRandomString();
+               $value1 = wfRandomString();
+               $value2 = wfRandomString();
+
+               $mockWallClock = 1549343530.2053;
+               $cache->setMockTime( $mockWallClock );
+
+               // Fake initial check key to be set in the past. Otherwise we'd have to sleep for
+               // several seconds during the test to assert the behaviour.
+               foreach ( [ $checkAll, $check1, $check2 ] as $checkKey ) {
+                       $cache->touchCheckKey( $checkKey, WANObjectCache::HOLDOFF_NONE );
+               }
+
+               $mockWallClock += 0.100;
+
+               $cache->set( 'key1', $value1, 10 );
+               $cache->set( 'key2', $value2, 10 );
+
+               $curTTLs = [];
+               $result = $cache->getMulti( [ 'key1', 'key2', 'key3' ], $curTTLs, [
+                       'key1' => $check1,
+                       $checkAll,
+                       'key2' => $check2,
+                       'key3' => $check3,
+               ] );
+               $this->assertEquals(
+                       [ 'key1' => $value1, 'key2' => $value2 ],
+                       $result,
+                       'Initial values'
+               );
+               $this->assertGreaterThanOrEqual( 9.5, $curTTLs['key1'], 'Initial ttls' );
+               $this->assertLessThanOrEqual( 10.5, $curTTLs['key1'], 'Initial ttls' );
+               $this->assertGreaterThanOrEqual( 9.5, $curTTLs['key2'], 'Initial ttls' );
+               $this->assertLessThanOrEqual( 10.5, $curTTLs['key2'], 'Initial ttls' );
+
+               $mockWallClock += 0.100;
+               $cache->touchCheckKey( $check1 );
+
+               $curTTLs = [];
+               $result = $cache->getMulti( [ 'key1', 'key2', 'key3' ], $curTTLs, [
+                       'key1' => $check1,
+                       $checkAll,
+                       'key2' => $check2,
+                       'key3' => $check3,
+               ] );
+               $this->assertEquals(
+                       [ 'key1' => $value1, 'key2' => $value2 ],
+                       $result,
+                       'key1 expired by check1, but value still provided'
+               );
+               $this->assertLessThan( 0, $curTTLs['key1'], 'key1 TTL expired' );
+               $this->assertGreaterThan( 0, $curTTLs['key2'], 'key2 still valid' );
+
+               $cache->touchCheckKey( $checkAll );
+
+               $curTTLs = [];
+               $result = $cache->getMulti( [ 'key1', 'key2', 'key3' ], $curTTLs, [
+                       'key1' => $check1,
+                       $checkAll,
+                       'key2' => $check2,
+                       'key3' => $check3,
+               ] );
+               $this->assertEquals(
+                       [ 'key1' => $value1, 'key2' => $value2 ],
+                       $result,
+                       'All keys expired by checkAll, but value still provided'
+               );
+               $this->assertLessThan( 0, $curTTLs['key1'], 'key1 expired by checkAll' );
+               $this->assertLessThan( 0, $curTTLs['key2'], 'key2 expired by checkAll' );
+       }
+
+       /**
+        * @covers WANObjectCache::get()
+        * @covers WANObjectCache::processCheckKeys()
+        */
+       public function testCheckKeyInitHoldoff() {
+               $cache = $this->cache;
+
+               for ( $i = 0; $i < 500; ++$i ) {
+                       $key = wfRandomString();
+                       $checkKey = wfRandomString();
+                       // miss, set, hit
+                       $cache->get( $key, $curTTL, [ $checkKey ] );
+                       $cache->set( $key, 'val', 10 );
+                       $curTTL = null;
+                       $v = $cache->get( $key, $curTTL, [ $checkKey ] );
+
+                       $this->assertEquals( 'val', $v );
+                       $this->assertLessThan( 0, $curTTL, "Step $i: CTL < 0 (miss/set/hit)" );
+               }
+
+               for ( $i = 0; $i < 500; ++$i ) {
+                       $key = wfRandomString();
+                       $checkKey = wfRandomString();
+                       // set, hit
+                       $cache->set( $key, 'val', 10 );
+                       $curTTL = null;
+                       $v = $cache->get( $key, $curTTL, [ $checkKey ] );
+
+                       $this->assertEquals( 'val', $v );
+                       $this->assertLessThan( 0, $curTTL, "Step $i: CTL < 0 (set/hit)" );
+               }
+       }
+
+       /**
+        * @covers WANObjectCache::delete
+        * @covers WANObjectCache::relayDelete
+        * @covers WANObjectCache::relayPurge
+        */
+       public function testDelete() {
+               $key = wfRandomString();
+               $value = wfRandomString();
+               $this->cache->set( $key, $value );
+
+               $curTTL = null;
+               $v = $this->cache->get( $key, $curTTL );
+               $this->assertEquals( $value, $v, "Key was created with value" );
+               $this->assertGreaterThan( 0, $curTTL, "Existing key has current TTL > 0" );
+
+               $this->cache->delete( $key );
+
+               $curTTL = null;
+               $v = $this->cache->get( $key, $curTTL );
+               $this->assertFalse( $v, "Deleted key has false value" );
+               $this->assertLessThan( 0, $curTTL, "Deleted key has current TTL < 0" );
+
+               $this->cache->set( $key, $value . 'more' );
+               $v = $this->cache->get( $key, $curTTL );
+               $this->assertFalse( $v, "Deleted key is tombstoned and has false value" );
+               $this->assertLessThan( 0, $curTTL, "Deleted key is tombstoned and has current TTL < 0" );
+
+               $this->cache->set( $key, $value );
+               $this->cache->delete( $key, WANObjectCache::HOLDOFF_NONE );
+
+               $curTTL = null;
+               $v = $this->cache->get( $key, $curTTL );
+               $this->assertFalse( $v, "Deleted key has false value" );
+               $this->assertNull( $curTTL, "Deleted key has null current TTL" );
+
+               $this->cache->set( $key, $value );
+               $v = $this->cache->get( $key, $curTTL );
+               $this->assertEquals( $value, $v, "Key was created with value" );
+               $this->assertGreaterThan( 0, $curTTL, "Existing key has current TTL > 0" );
+       }
+
+       /**
+        * @dataProvider getWithSetCallback_versions_provider
+        * @covers WANObjectCache::getWithSetCallback()
+        * @covers WANObjectCache::doGetWithSetCallback()
+        * @param array $extOpts
+        * @param bool $versioned
+        */
+       public function testGetWithSetCallback_versions( array $extOpts, $versioned ) {
+               $cache = $this->cache;
+
+               $key = wfRandomString();
+               $valueV1 = wfRandomString();
+               $valueV2 = [ wfRandomString() ];
+
+               $wasSet = 0;
+               $funcV1 = function () use ( &$wasSet, $valueV1 ) {
+                       ++$wasSet;
+
+                       return $valueV1;
+               };
+
+               $priorValue = false;
+               $priorAsOf = null;
+               $funcV2 = function ( $oldValue, &$ttl, $setOpts, $oldAsOf )
+               use ( &$wasSet, $valueV2, &$priorValue, &$priorAsOf ) {
+                       $priorValue = $oldValue;
+                       $priorAsOf = $oldAsOf;
+                       ++$wasSet;
+
+                       return $valueV2; // new array format
+               };
+
+               // Set the main key (version N if versioned)
+               $wasSet = 0;
+               $v = $cache->getWithSetCallback( $key, 30, $funcV1, $extOpts );
+               $this->assertEquals( $valueV1, $v, "Value returned" );
+               $this->assertEquals( 1, $wasSet, "Value regenerated" );
+               $cache->getWithSetCallback( $key, 30, $funcV1, $extOpts );
+               $this->assertEquals( 1, $wasSet, "Value not regenerated" );
+               $this->assertEquals( $valueV1, $v, "Value not regenerated" );
+
+               if ( $versioned ) {
+                       // Set the key for version N+1 format
+                       $verOpts = [ 'version' => $extOpts['version'] + 1 ];
+               } else {
+                       // Start versioning now with the unversioned key still there
+                       $verOpts = [ 'version' => 1 ];
+               }
+
+               // Value goes to secondary key since V1 already used $key
+               $wasSet = 0;
+               $v = $cache->getWithSetCallback( $key, 30, $funcV2, $verOpts + $extOpts );
+               $this->assertEquals( $valueV2, $v, "Value returned" );
+               $this->assertEquals( 1, $wasSet, "Value regenerated" );
+               $this->assertEquals( false, $priorValue, "Old value not given due to old format" );
+               $this->assertEquals( null, $priorAsOf, "Old value not given due to old format" );
+
+               $wasSet = 0;
+               $v = $cache->getWithSetCallback( $key, 30, $funcV2, $verOpts + $extOpts );
+               $this->assertEquals( $valueV2, $v, "Value not regenerated (secondary key)" );
+               $this->assertEquals( 0, $wasSet, "Value not regenerated (secondary key)" );
+
+               // Clear out the older or unversioned key
+               $cache->delete( $key, 0 );
+
+               // Set the key for next/first versioned format
+               $wasSet = 0;
+               $v = $cache->getWithSetCallback( $key, 30, $funcV2, $verOpts + $extOpts );
+               $this->assertEquals( $valueV2, $v, "Value returned" );
+               $this->assertEquals( 1, $wasSet, "Value regenerated" );
+
+               $v = $cache->getWithSetCallback( $key, 30, $funcV2, $verOpts + $extOpts );
+               $this->assertEquals( $valueV2, $v, "Value not regenerated (main key)" );
+               $this->assertEquals( 1, $wasSet, "Value not regenerated (main key)" );
+       }
+
+       public static function getWithSetCallback_versions_provider() {
+               return [
+                       [ [], false ],
+                       [ [ 'version' => 1 ], true ]
+               ];
+       }
+
+       /**
+        * @covers WANObjectCache::useInterimHoldOffCaching
+        * @covers WANObjectCache::getInterimValue
+        */
+       public function testInterimHoldOffCaching() {
+               $cache = $this->cache;
+
+               $mockWallClock = 1549343530.2053;
+               $cache->setMockTime( $mockWallClock );
+
+               $value = 'CRL-40-940';
+               $wasCalled = 0;
+               $func = function () use ( &$wasCalled, $value ) {
+                       $wasCalled++;
+
+                       return $value;
+               };
+
+               $cache->useInterimHoldOffCaching( true );
+
+               $key = wfRandomString( 32 );
+               $v = $cache->getWithSetCallback( $key, 60, $func );
+               $v = $cache->getWithSetCallback( $key, 60, $func );
+               $this->assertEquals( 1, $wasCalled, 'Value cached' );
+
+               $cache->delete( $key );
+               $mockWallClock += 0.001; // cached values will be newer than tombstone
+               $v = $cache->getWithSetCallback( $key, 60, $func );
+               $this->assertEquals( 2, $wasCalled, 'Value regenerated (got mutex)' ); // sets interim
+               $v = $cache->getWithSetCallback( $key, 60, $func );
+               $this->assertEquals( 2, $wasCalled, 'Value interim cached' ); // reuses interim
+
+               $mockWallClock += 0.2; // interim key not brand new
+               $v = $cache->getWithSetCallback( $key, 60, $func );
+               $this->assertEquals( 3, $wasCalled, 'Value regenerated (got mutex)' ); // sets interim
+               // Lock up the mutex so interim cache is used
+               $this->internalCache->add( $cache::MUTEX_KEY_PREFIX . $key, 1, 0 );
+               $v = $cache->getWithSetCallback( $key, 60, $func );
+               $this->assertEquals( 3, $wasCalled, 'Value interim cached (failed mutex)' );
+               $this->internalCache->delete( $cache::MUTEX_KEY_PREFIX . $key );
+
+               $cache->useInterimHoldOffCaching( false );
+
+               $wasCalled = 0;
+               $key = wfRandomString( 32 );
+               $v = $cache->getWithSetCallback( $key, 60, $func );
+               $v = $cache->getWithSetCallback( $key, 60, $func );
+               $this->assertEquals( 1, $wasCalled, 'Value cached' );
+               $cache->delete( $key );
+               $v = $cache->getWithSetCallback( $key, 60, $func );
+               $this->assertEquals( 2, $wasCalled, 'Value regenerated (got mutex)' );
+               $v = $cache->getWithSetCallback( $key, 60, $func );
+               $this->assertEquals( 3, $wasCalled, 'Value still regenerated (got mutex)' );
+               $v = $cache->getWithSetCallback( $key, 60, $func );
+               $this->assertEquals( 4, $wasCalled, 'Value still regenerated (got mutex)' );
+               // Lock up the mutex so interim cache is used
+               $this->internalCache->add( $cache::MUTEX_KEY_PREFIX . $key, 1, 0 );
+               $v = $cache->getWithSetCallback( $key, 60, $func );
+               $this->assertEquals( 5, $wasCalled, 'Value still regenerated (failed mutex)' );
+       }
+
+       /**
+        * @covers WANObjectCache::touchCheckKey
+        * @covers WANObjectCache::resetCheckKey
+        * @covers WANObjectCache::getCheckKeyTime
+        * @covers WANObjectCache::getMultiCheckKeyTime
+        * @covers WANObjectCache::makePurgeValue
+        * @covers WANObjectCache::parsePurgeValue
+        */
+       public function testTouchKeys() {
+               $cache = $this->cache;
+               $key = wfRandomString();
+
+               $mockWallClock = 1549343530.2053;
+               $priorTime = $mockWallClock; // reference time
+               $cache->setMockTime( $mockWallClock );
+
+               $mockWallClock += 0.100;
+               $t0 = $cache->getCheckKeyTime( $key );
+               $this->assertGreaterThanOrEqual( $priorTime, $t0, 'Check key auto-created' );
+
+               $priorTime = $mockWallClock;
+               $mockWallClock += 0.100;
+               $cache->touchCheckKey( $key );
+               $t1 = $cache->getCheckKeyTime( $key );
+               $this->assertGreaterThanOrEqual( $priorTime, $t1, 'Check key created' );
+
+               $t2 = $cache->getCheckKeyTime( $key );
+               $this->assertEquals( $t1, $t2, 'Check key time did not change' );
+
+               $mockWallClock += 0.100;
+               $cache->touchCheckKey( $key );
+               $t3 = $cache->getCheckKeyTime( $key );
+               $this->assertGreaterThan( $t2, $t3, 'Check key time increased' );
+
+               $t4 = $cache->getCheckKeyTime( $key );
+               $this->assertEquals( $t3, $t4, 'Check key time did not change' );
+
+               $mockWallClock += 0.100;
+               $cache->resetCheckKey( $key );
+               $t5 = $cache->getCheckKeyTime( $key );
+               $this->assertGreaterThan( $t4, $t5, 'Check key time increased' );
+
+               $t6 = $cache->getCheckKeyTime( $key );
+               $this->assertEquals( $t5, $t6, 'Check key time did not change' );
+       }
+
+       /**
+        * @covers WANObjectCache::getMulti()
+        */
+       public function testGetWithSeveralCheckKeys() {
+               $key = wfRandomString();
+               $tKey1 = wfRandomString();
+               $tKey2 = wfRandomString();
+               $value = 'meow';
+
+               $mockWallClock = 1549343530.2053;
+               $priorTime = $mockWallClock; // reference time
+               $this->cache->setMockTime( $mockWallClock );
+
+               // Two check keys are newer (given hold-off) than $key, another is older
+               $this->internalCache->set(
+                       WANObjectCache::TIME_KEY_PREFIX . $tKey2,
+                       WANObjectCache::PURGE_VAL_PREFIX . ( $priorTime - 3 )
+               );
+               $this->internalCache->set(
+                       WANObjectCache::TIME_KEY_PREFIX . $tKey2,
+                       WANObjectCache::PURGE_VAL_PREFIX . ( $priorTime - 5 )
+               );
+               $this->internalCache->set(
+                       WANObjectCache::TIME_KEY_PREFIX . $tKey1,
+                       WANObjectCache::PURGE_VAL_PREFIX . ( $priorTime - 30 )
+               );
+               $this->cache->set( $key, $value, 30 );
+
+               $curTTL = null;
+               $v = $this->cache->get( $key, $curTTL, [ $tKey1, $tKey2 ] );
+               $this->assertEquals( $value, $v, "Value matches" );
+               $this->assertLessThan( -4.9, $curTTL, "Correct CTL" );
+               $this->assertGreaterThan( -5.1, $curTTL, "Correct CTL" );
+       }
+
+       /**
+        * @covers WANObjectCache::reap()
+        * @covers WANObjectCache::reapCheckKey()
+        */
+       public function testReap() {
+               $vKey1 = wfRandomString();
+               $vKey2 = wfRandomString();
+               $tKey1 = wfRandomString();
+               $tKey2 = wfRandomString();
+               $value = 'moo';
+
+               $knownPurge = time() - 60;
+               $goodTime = microtime( true ) - 5;
+               $badTime = microtime( true ) - 300;
+
+               $this->internalCache->set(
+                       WANObjectCache::VALUE_KEY_PREFIX . $vKey1,
+                       [
+                               WANObjectCache::FLD_VERSION => WANObjectCache::VERSION,
+                               WANObjectCache::FLD_VALUE => $value,
+                               WANObjectCache::FLD_TTL => 3600,
+                               WANObjectCache::FLD_TIME => $goodTime
+                       ]
+               );
+               $this->internalCache->set(
+                       WANObjectCache::VALUE_KEY_PREFIX . $vKey2,
+                       [
+                               WANObjectCache::FLD_VERSION => WANObjectCache::VERSION,
+                               WANObjectCache::FLD_VALUE => $value,
+                               WANObjectCache::FLD_TTL => 3600,
+                               WANObjectCache::FLD_TIME => $badTime
+                       ]
+               );
+               $this->internalCache->set(
+                       WANObjectCache::TIME_KEY_PREFIX . $tKey1,
+                       WANObjectCache::PURGE_VAL_PREFIX . $goodTime
+               );
+               $this->internalCache->set(
+                       WANObjectCache::TIME_KEY_PREFIX . $tKey2,
+                       WANObjectCache::PURGE_VAL_PREFIX . $badTime
+               );
+
+               $this->assertEquals( $value, $this->cache->get( $vKey1 ) );
+               $this->assertEquals( $value, $this->cache->get( $vKey2 ) );
+               $this->cache->reap( $vKey1, $knownPurge, $bad1 );
+               $this->cache->reap( $vKey2, $knownPurge, $bad2 );
+
+               $this->assertFalse( $bad1 );
+               $this->assertTrue( $bad2 );
+
+               $this->cache->reapCheckKey( $tKey1, $knownPurge, $tBad1 );
+               $this->cache->reapCheckKey( $tKey2, $knownPurge, $tBad2 );
+               $this->assertFalse( $tBad1 );
+               $this->assertTrue( $tBad2 );
+       }
+
+       /**
+        * @covers WANObjectCache::reap()
+        */
+       public function testReap_fail() {
+               $backend = $this->getMockBuilder( EmptyBagOStuff::class )
+                       ->setMethods( [ 'get', 'changeTTL' ] )->getMock();
+               $backend->expects( $this->once() )->method( 'get' )
+                       ->willReturn( [
+                               WANObjectCache::FLD_VERSION => WANObjectCache::VERSION,
+                               WANObjectCache::FLD_VALUE => 'value',
+                               WANObjectCache::FLD_TTL => 3600,
+                               WANObjectCache::FLD_TIME => 300,
+                       ] );
+               $backend->expects( $this->once() )->method( 'changeTTL' )
+                       ->willReturn( false );
+
+               $wanCache = new WANObjectCache( [
+                       'cache' => $backend
+               ] );
+
+               $isStale = null;
+               $ret = $wanCache->reap( 'key', 360, $isStale );
+               $this->assertTrue( $isStale, 'value was stale' );
+               $this->assertFalse( $ret, 'changeTTL failed' );
+       }
+
+       /**
+        * @covers WANObjectCache::set()
+        */
+       public function testSetWithLag() {
+               $value = 1;
+
+               $key = wfRandomString();
+               $opts = [ 'lag' => 300, 'since' => microtime( true ) ];
+               $this->cache->set( $key, $value, 30, $opts );
+               $this->assertEquals( $value, $this->cache->get( $key ), "Rep-lagged value written." );
+
+               $key = wfRandomString();
+               $opts = [ 'lag' => 0, 'since' => microtime( true ) - 300 ];
+               $this->cache->set( $key, $value, 30, $opts );
+               $this->assertEquals( false, $this->cache->get( $key ), "Trx-lagged value not written." );
+
+               $key = wfRandomString();
+               $opts = [ 'lag' => 5, 'since' => microtime( true ) - 5 ];
+               $this->cache->set( $key, $value, 30, $opts );
+               $this->assertEquals( false, $this->cache->get( $key ), "Lagged value not written." );
+       }
+
+       /**
+        * @covers WANObjectCache::set()
+        */
+       public function testWritePending() {
+               $value = 1;
+
+               $key = wfRandomString();
+               $opts = [ 'pending' => true ];
+               $this->cache->set( $key, $value, 30, $opts );
+               $this->assertEquals( false, $this->cache->get( $key ), "Pending value not written." );
+       }
+
+       public function testMcRouterSupport() {
+               $localBag = $this->getMockBuilder( EmptyBagOStuff::class )
+                       ->setMethods( [ 'set', 'delete' ] )->getMock();
+               $localBag->expects( $this->never() )->method( 'set' );
+               $localBag->expects( $this->never() )->method( 'delete' );
+               $wanCache = new WANObjectCache( [
+                       'cache' => $localBag,
+                       'mcrouterAware' => true,
+                       'region' => 'pmtpa',
+                       'cluster' => 'mw-wan'
+               ] );
+               $valFunc = function () {
+                       return 1;
+               };
+
+               // None of these should use broadcasting commands (e.g. SET, DELETE)
+               $wanCache->get( 'x' );
+               $wanCache->get( 'x', $ctl, [ 'check1' ] );
+               $wanCache->getMulti( [ 'x', 'y' ] );
+               $wanCache->getMulti( [ 'x', 'y' ], $ctls, [ 'check2' ] );
+               $wanCache->getWithSetCallback( 'p', 30, $valFunc );
+               $wanCache->getCheckKeyTime( 'zzz' );
+               $wanCache->reap( 'x', time() - 300 );
+               $wanCache->reap( 'zzz', time() - 300 );
+       }
+
+       public function testMcRouterSupportBroadcastDelete() {
+               $localBag = $this->getMockBuilder( EmptyBagOStuff::class )
+                       ->setMethods( [ 'set' ] )->getMock();
+               $wanCache = new WANObjectCache( [
+                       'cache' => $localBag,
+                       'mcrouterAware' => true,
+                       'region' => 'pmtpa',
+                       'cluster' => 'mw-wan'
+               ] );
+
+               $localBag->expects( $this->once() )->method( 'set' )
+                       ->with( "/*/mw-wan/" . $wanCache::VALUE_KEY_PREFIX . "test" );
+
+               $wanCache->delete( 'test' );
+       }
+
+       public function testMcRouterSupportBroadcastTouchCK() {
+               $localBag = $this->getMockBuilder( EmptyBagOStuff::class )
+                       ->setMethods( [ 'set' ] )->getMock();
+               $wanCache = new WANObjectCache( [
+                       'cache' => $localBag,
+                       'mcrouterAware' => true,
+                       'region' => 'pmtpa',
+                       'cluster' => 'mw-wan'
+               ] );
+
+               $localBag->expects( $this->once() )->method( 'set' )
+                       ->with( "/*/mw-wan/" . $wanCache::TIME_KEY_PREFIX . "test" );
+
+               $wanCache->touchCheckKey( 'test' );
+       }
+
+       public function testMcRouterSupportBroadcastResetCK() {
+               $localBag = $this->getMockBuilder( EmptyBagOStuff::class )
+                       ->setMethods( [ 'delete' ] )->getMock();
+               $wanCache = new WANObjectCache( [
+                       'cache' => $localBag,
+                       'mcrouterAware' => true,
+                       'region' => 'pmtpa',
+                       'cluster' => 'mw-wan'
+               ] );
+
+               $localBag->expects( $this->once() )->method( 'delete' )
+                       ->with( "/*/mw-wan/" . $wanCache::TIME_KEY_PREFIX . "test" );
+
+               $wanCache->resetCheckKey( 'test' );
+       }
+
+       public function testEpoch() {
+               $bag = new HashBagOStuff();
+               $cache = new WANObjectCache( [ 'cache' => $bag ] );
+               $key = $cache->makeGlobalKey( 'The whole of the Law' );
+
+               $now = microtime( true );
+               $cache->setMockTime( $now );
+
+               $cache->set( $key, 'Do what thou Wilt' );
+               $cache->touchCheckKey( $key );
+
+               $then = $now;
+               $now += 30;
+               $this->assertEquals( 'Do what thou Wilt', $cache->get( $key ) );
+               $this->assertEquals( $then, $cache->getCheckKeyTime( $key ), 'Check key init', 0.01 );
+
+               $cache = new WANObjectCache( [
+                       'cache' => $bag,
+                       'epoch' => $now - 3600
+               ] );
+               $cache->setMockTime( $now );
+
+               $this->assertEquals( 'Do what thou Wilt', $cache->get( $key ) );
+               $this->assertEquals( $then, $cache->getCheckKeyTime( $key ), 'Check key kept', 0.01 );
+
+               $now += 30;
+               $cache = new WANObjectCache( [
+                       'cache' => $bag,
+                       'epoch' => $now + 3600
+               ] );
+               $cache->setMockTime( $now );
+
+               $this->assertFalse( $cache->get( $key ), 'Key rejected due to epoch' );
+               $this->assertEquals( $now, $cache->getCheckKeyTime( $key ), 'Check key reset', 0.01 );
+       }
+
+       /**
+        * @dataProvider provideAdaptiveTTL
+        * @covers WANObjectCache::adaptiveTTL()
+        * @param float|int $ago
+        * @param int $maxTTL
+        * @param int $minTTL
+        * @param float $factor
+        * @param int $adaptiveTTL
+        */
+       public function testAdaptiveTTL( $ago, $maxTTL, $minTTL, $factor, $adaptiveTTL ) {
+               $mtime = $ago ? time() - $ago : $ago;
+               $margin = 5;
+               $ttl = $this->cache->adaptiveTTL( $mtime, $maxTTL, $minTTL, $factor );
+
+               $this->assertGreaterThanOrEqual( $adaptiveTTL - $margin, $ttl );
+               $this->assertLessThanOrEqual( $adaptiveTTL + $margin, $ttl );
+
+               $ttl = $this->cache->adaptiveTTL( (string)$mtime, $maxTTL, $minTTL, $factor );
+
+               $this->assertGreaterThanOrEqual( $adaptiveTTL - $margin, $ttl );
+               $this->assertLessThanOrEqual( $adaptiveTTL + $margin, $ttl );
+       }
+
+       public static function provideAdaptiveTTL() {
+               return [
+                       [ 3600, 900, 30, 0.2, 720 ],
+                       [ 3600, 500, 30, 0.2, 500 ],
+                       [ 3600, 86400, 800, 0.2, 800 ],
+                       [ false, 86400, 800, 0.2, 800 ],
+                       [ null, 86400, 800, 0.2, 800 ]
+               ];
+       }
+
+       /**
+        * @covers WANObjectCache::__construct
+        * @covers WANObjectCache::newEmpty
+        */
+       public function testNewEmpty() {
+               $this->assertInstanceOf(
+                       WANObjectCache::class,
+                       WANObjectCache::newEmpty()
+               );
+       }
+
+       /**
+        * @covers WANObjectCache::setLogger
+        */
+       public function testSetLogger() {
+               $this->assertSame( null, $this->cache->setLogger( new Psr\Log\NullLogger ) );
+       }
+
+       /**
+        * @covers WANObjectCache::getQoS
+        */
+       public function testGetQoS() {
+               $backend = $this->getMockBuilder( HashBagOStuff::class )
+                       ->setMethods( [ 'getQoS' ] )->getMock();
+               $backend->expects( $this->once() )->method( 'getQoS' )
+                       ->willReturn( BagOStuff::QOS_UNKNOWN );
+               $wanCache = new WANObjectCache( [ 'cache' => $backend ] );
+
+               $this->assertSame(
+                       $wanCache::QOS_UNKNOWN,
+                       $wanCache->getQoS( $wanCache::ATTR_EMULATION )
+               );
+       }
+
+       /**
+        * @covers WANObjectCache::makeKey
+        */
+       public function testMakeKey() {
+               $backend = $this->getMockBuilder( HashBagOStuff::class )
+                       ->setMethods( [ 'makeKey' ] )->getMock();
+               $backend->expects( $this->once() )->method( 'makeKey' )
+                       ->willReturn( 'special' );
+
+               $wanCache = new WANObjectCache( [
+                       'cache' => $backend
+               ] );
+
+               $this->assertSame( 'special', $wanCache->makeKey( 'a', 'b' ) );
+       }
+
+       /**
+        * @covers WANObjectCache::makeGlobalKey
+        */
+       public function testMakeGlobalKey() {
+               $backend = $this->getMockBuilder( HashBagOStuff::class )
+                       ->setMethods( [ 'makeGlobalKey' ] )->getMock();
+               $backend->expects( $this->once() )->method( 'makeGlobalKey' )
+                       ->willReturn( 'special' );
+
+               $wanCache = new WANObjectCache( [
+                       'cache' => $backend
+               ] );
+
+               $this->assertSame( 'special', $wanCache->makeGlobalKey( 'a', 'b' ) );
+       }
+
+       public static function statsKeyProvider() {
+               return [
+                       [ 'domain:page:5', 'page' ],
+                       [ 'domain:main-key', 'main-key' ],
+                       [ 'domain:page:history', 'page' ],
+                       [ 'missingdomainkey', 'missingdomainkey' ]
+               ];
+       }
+
+       /**
+        * @dataProvider statsKeyProvider
+        * @covers WANObjectCache::determineKeyClassForStats
+        */
+       public function testStatsKeyClass( $key, $class ) {
+               $wanCache = TestingAccessWrapper::newFromObject( new WANObjectCache( [
+                       'cache' => new HashBagOStuff
+               ] ) );
+
+               $this->assertEquals( $class, $wanCache->determineKeyClassForStats( $key ) );
+       }
+}
+
+class NearExpiringWANObjectCache extends WANObjectCache {
+       const CLOCK_SKEW = 1;
+
+       protected function worthRefreshExpiring( $curTTL, $lowTTL ) {
+               return ( $curTTL > 0 && ( $curTTL + self::CLOCK_SKEW ) < $lowTTL );
+       }
+}
+
+class PopularityRefreshingWANObjectCache extends WANObjectCache {
+       protected function worthRefreshPopular( $asOf, $ageNew, $timeTillRefresh, $now ) {
+               return ( ( $now - $asOf ) > $timeTillRefresh );
+       }
+}
diff --git a/tests/phpunit/unit/includes/libs/rdbms/ChronologyProtectorTest.php b/tests/phpunit/unit/includes/libs/rdbms/ChronologyProtectorTest.php
new file mode 100644 (file)
index 0000000..5901bc1
--- /dev/null
@@ -0,0 +1,81 @@
+<?php
+
+/**
+ * Holds tests for ChronologyProtector abstract MediaWiki class.
+ *
+ * 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; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * 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.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+use Wikimedia\Rdbms\ChronologyProtector;
+
+/**
+ * @group Database
+ * @covers \Wikimedia\Rdbms\ChronologyProtector::__construct
+ * @covers \Wikimedia\Rdbms\ChronologyProtector::getClientId
+ */
+class ChronologyProtectorTest extends PHPUnit\Framework\TestCase {
+       /**
+        * @dataProvider clientIdProvider
+        * @param array $client
+        * @param string $secret
+        * @param string $expectedId
+        */
+       public function testClientId( array $client, $secret, $expectedId ) {
+               $bag = new HashBagOStuff();
+               $cp = new ChronologyProtector( $bag, $client, null, $secret );
+
+               $this->assertEquals( $expectedId, $cp->getClientId() );
+       }
+
+       public function clientIdProvider() {
+               return [
+                       [
+                               [
+                                       'ip' => '127.0.0.1',
+                                       'agent' => "Totally-Not-FireFox"
+                               ],
+                               '',
+                               '45e93a9c215c031d38b7c42d8e4700ca',
+                       ],
+                       [
+                               [
+                                       'ip' => '127.0.0.7',
+                                       'agent' => "Totally-Not-FireFox"
+                               ],
+                               '',
+                               'b1d604117b51746c35c3df9f293c84dc'
+                       ],
+                       [
+                               [
+                                       'ip' => '127.0.0.1',
+                                       'agent' => "Totally-FireFox"
+                               ],
+                               '',
+                               '731b4e06a65e2346b497fc811571c4d7'
+                       ],
+                       [
+                               [
+                                       'ip' => '127.0.0.1',
+                                       'agent' => "Totally-Not-FireFox"
+                               ],
+                               'secret',
+                               'defff51ded73cd901253d874c9b2077d'
+                       ]
+               ];
+       }
+}
diff --git a/tests/phpunit/unit/includes/libs/rdbms/TransactionProfilerTest.php b/tests/phpunit/unit/includes/libs/rdbms/TransactionProfilerTest.php
new file mode 100644 (file)
index 0000000..538d625
--- /dev/null
@@ -0,0 +1,147 @@
+<?php
+
+use Wikimedia\Rdbms\TransactionProfiler;
+use Psr\Log\LoggerInterface;
+
+/**
+ * @covers \Wikimedia\Rdbms\TransactionProfiler
+ */
+class TransactionProfilerTest extends PHPUnit\Framework\TestCase {
+
+       use MediaWikiCoversValidator;
+
+       public function testAffected() {
+               $logger = $this->getMockBuilder( LoggerInterface::class )->getMock();
+               $logger->expects( $this->exactly( 3 ) )->method( 'warning' );
+
+               $tp = new TransactionProfiler();
+               $tp->setLogger( $logger );
+               $tp->setExpectation( 'maxAffected', 100, __METHOD__ );
+
+               $tp->transactionWritingIn( 'srv1', 'db1', '123' );
+               $tp->recordQueryCompletion( "SQL 1", microtime( true ) - 3, true, 200 );
+               $tp->recordQueryCompletion( "SQL 2", microtime( true ) - 3, true, 200 );
+               $tp->transactionWritingOut( 'srv1', 'db1', '123', 1, 400 );
+       }
+
+       public function testReadTime() {
+               $logger = $this->getMockBuilder( LoggerInterface::class )->getMock();
+               // 1 per query
+               $logger->expects( $this->exactly( 2 ) )->method( 'warning' );
+
+               $tp = new TransactionProfiler();
+               $tp->setLogger( $logger );
+               $tp->setExpectation( 'readQueryTime', 5, __METHOD__ );
+
+               $tp->transactionWritingIn( 'srv1', 'db1', '123' );
+               $tp->recordQueryCompletion( "SQL 1", microtime( true ) - 10, false, 1 );
+               $tp->recordQueryCompletion( "SQL 2", microtime( true ) - 10, false, 1 );
+               $tp->transactionWritingOut( 'srv1', 'db1', '123', 0, 0 );
+       }
+
+       public function testWriteTime() {
+               $logger = $this->getMockBuilder( LoggerInterface::class )->getMock();
+               // 1 per query, 1 per trx, and one "sub-optimal trx" entry
+               $logger->expects( $this->exactly( 4 ) )->method( 'warning' );
+
+               $tp = new TransactionProfiler();
+               $tp->setLogger( $logger );
+               $tp->setExpectation( 'writeQueryTime', 5, __METHOD__ );
+
+               $tp->transactionWritingIn( 'srv1', 'db1', '123' );
+               $tp->recordQueryCompletion( "SQL 1", microtime( true ) - 10, true, 1 );
+               $tp->recordQueryCompletion( "SQL 2", microtime( true ) - 10, true, 1 );
+               $tp->transactionWritingOut( 'srv1', 'db1', '123', 20, 1 );
+       }
+
+       public function testAffectedTrx() {
+               $logger = $this->getMockBuilder( LoggerInterface::class )->getMock();
+               $logger->expects( $this->exactly( 1 ) )->method( 'warning' );
+
+               $tp = new TransactionProfiler();
+               $tp->setLogger( $logger );
+               $tp->setExpectation( 'maxAffected', 100, __METHOD__ );
+
+               $tp->transactionWritingIn( 'srv1', 'db1', '123' );
+               $tp->transactionWritingOut( 'srv1', 'db1', '123', 1, 200 );
+       }
+
+       public function testWriteTimeTrx() {
+               $logger = $this->getMockBuilder( LoggerInterface::class )->getMock();
+               // 1 per trx, and one "sub-optimal trx" entry
+               $logger->expects( $this->exactly( 2 ) )->method( 'warning' );
+
+               $tp = new TransactionProfiler();
+               $tp->setLogger( $logger );
+               $tp->setExpectation( 'writeQueryTime', 5, __METHOD__ );
+
+               $tp->transactionWritingIn( 'srv1', 'db1', '123' );
+               $tp->transactionWritingOut( 'srv1', 'db1', '123', 10, 1 );
+       }
+
+       public function testConns() {
+               $logger = $this->getMockBuilder( LoggerInterface::class )->getMock();
+               $logger->expects( $this->exactly( 2 ) )->method( 'warning' );
+
+               $tp = new TransactionProfiler();
+               $tp->setLogger( $logger );
+               $tp->setExpectation( 'conns', 2, __METHOD__ );
+
+               $tp->recordConnection( 'srv1', 'db1', false );
+               $tp->recordConnection( 'srv1', 'db2', false );
+               $tp->recordConnection( 'srv1', 'db3', false ); // warn
+               $tp->recordConnection( 'srv1', 'db4', false ); // warn
+       }
+
+       public function testMasterConns() {
+               $logger = $this->getMockBuilder( LoggerInterface::class )->getMock();
+               $logger->expects( $this->exactly( 2 ) )->method( 'warning' );
+
+               $tp = new TransactionProfiler();
+               $tp->setLogger( $logger );
+               $tp->setExpectation( 'masterConns', 2, __METHOD__ );
+
+               $tp->recordConnection( 'srv1', 'db1', false );
+               $tp->recordConnection( 'srv1', 'db2', false );
+
+               $tp->recordConnection( 'srv1', 'db1', true );
+               $tp->recordConnection( 'srv1', 'db2', true );
+               $tp->recordConnection( 'srv1', 'db3', true ); // warn
+               $tp->recordConnection( 'srv1', 'db4', true ); // warn
+       }
+
+       public function testReadQueryCount() {
+               $logger = $this->getMockBuilder( LoggerInterface::class )->getMock();
+               $logger->expects( $this->exactly( 2 ) )->method( 'warning' );
+
+               $tp = new TransactionProfiler();
+               $tp->setLogger( $logger );
+               $tp->setExpectation( 'queries', 2, __METHOD__ );
+
+               $tp->recordQueryCompletion( "SQL 1", microtime( true ) - 0.01, false, 0 );
+               $tp->recordQueryCompletion( "SQL 2", microtime( true ) - 0.01, false, 0 );
+               $tp->recordQueryCompletion( "SQL 3", microtime( true ) - 0.01, false, 0 ); // warn
+               $tp->recordQueryCompletion( "SQL 4", microtime( true ) - 0.01, false, 0 ); // warn
+       }
+
+       public function testWriteQueryCount() {
+               $logger = $this->getMockBuilder( LoggerInterface::class )->getMock();
+               $logger->expects( $this->exactly( 2 ) )->method( 'warning' );
+
+               $tp = new TransactionProfiler();
+               $tp->setLogger( $logger );
+               $tp->setExpectation( 'writes', 2, __METHOD__ );
+
+               $tp->recordQueryCompletion( "SQL 1", microtime( true ) - 0.01, false, 0 );
+               $tp->recordQueryCompletion( "SQL 2", microtime( true ) - 0.01, false, 0 );
+               $tp->recordQueryCompletion( "SQL 3", microtime( true ) - 0.01, false, 0 );
+               $tp->recordQueryCompletion( "SQL 4", microtime( true ) - 0.01, false, 0 );
+
+               $tp->transactionWritingIn( 'srv1', 'db1', '123' );
+               $tp->recordQueryCompletion( "SQL 1w", microtime( true ) - 0.01, true, 2 );
+               $tp->recordQueryCompletion( "SQL 2w", microtime( true ) - 0.01, true, 5 );
+               $tp->recordQueryCompletion( "SQL 3w", microtime( true ) - 0.01, true, 3 );
+               $tp->recordQueryCompletion( "SQL 4w", microtime( true ) - 0.01, true, 1 );
+               $tp->transactionWritingOut( 'srv1', 'db1', '123', 1, 1 );
+       }
+}
diff --git a/tests/phpunit/unit/includes/libs/rdbms/connectionmanager/ConnectionManagerTest.php b/tests/phpunit/unit/includes/libs/rdbms/connectionmanager/ConnectionManagerTest.php
new file mode 100644 (file)
index 0000000..dd86a73
--- /dev/null
@@ -0,0 +1,139 @@
+<?php
+
+namespace Wikimedia\Tests\Rdbms;
+
+use Wikimedia\Rdbms\IDatabase;
+use Wikimedia\Rdbms\LoadBalancer;
+use PHPUnit_Framework_MockObject_MockObject;
+use Wikimedia\Rdbms\ConnectionManager;
+
+/**
+ * @covers Wikimedia\Rdbms\ConnectionManager
+ *
+ * @author Daniel Kinzler
+ */
+class ConnectionManagerTest extends \PHPUnit\Framework\TestCase {
+
+       /**
+        * @return IDatabase|PHPUnit_Framework_MockObject_MockObject
+        */
+       private function getIDatabaseMock() {
+               return $this->getMockBuilder( IDatabase::class )
+                       ->getMock();
+       }
+
+       /**
+        * @return LoadBalancer|PHPUnit_Framework_MockObject_MockObject
+        */
+       private function getLoadBalancerMock() {
+               $lb = $this->getMockBuilder( LoadBalancer::class )
+                       ->disableOriginalConstructor()
+                       ->getMock();
+
+               return $lb;
+       }
+
+       public function testGetReadConnection_nullGroups() {
+               $database = $this->getIDatabaseMock();
+               $lb = $this->getLoadBalancerMock();
+
+               $lb->expects( $this->once() )
+                       ->method( 'getConnection' )
+                       ->with( DB_REPLICA, [ 'group1' ], 'someDbName' )
+                       ->will( $this->returnValue( $database ) );
+
+               $manager = new ConnectionManager( $lb, 'someDbName', [ 'group1' ] );
+               $actual = $manager->getReadConnection();
+
+               $this->assertSame( $database, $actual );
+       }
+
+       public function testGetReadConnection_withGroups() {
+               $database = $this->getIDatabaseMock();
+               $lb = $this->getLoadBalancerMock();
+
+               $lb->expects( $this->once() )
+                       ->method( 'getConnection' )
+                       ->with( DB_REPLICA, [ 'group2' ], 'someDbName' )
+                       ->will( $this->returnValue( $database ) );
+
+               $manager = new ConnectionManager( $lb, 'someDbName', [ 'group1' ] );
+               $actual = $manager->getReadConnection( [ 'group2' ] );
+
+               $this->assertSame( $database, $actual );
+       }
+
+       public function testGetWriteConnection() {
+               $database = $this->getIDatabaseMock();
+               $lb = $this->getLoadBalancerMock();
+
+               $lb->expects( $this->once() )
+                       ->method( 'getConnection' )
+                       ->with( DB_MASTER, [ 'group1' ], 'someDbName' )
+                       ->will( $this->returnValue( $database ) );
+
+               $manager = new ConnectionManager( $lb, 'someDbName', [ 'group1' ] );
+               $actual = $manager->getWriteConnection();
+
+               $this->assertSame( $database, $actual );
+       }
+
+       public function testReleaseConnection() {
+               $database = $this->getIDatabaseMock();
+               $lb = $this->getLoadBalancerMock();
+
+               $lb->expects( $this->once() )
+                       ->method( 'reuseConnection' )
+                       ->with( $database )
+                       ->will( $this->returnValue( null ) );
+
+               $manager = new ConnectionManager( $lb );
+               $manager->releaseConnection( $database );
+       }
+
+       public function testGetReadConnectionRef_nullGroups() {
+               $database = $this->getIDatabaseMock();
+               $lb = $this->getLoadBalancerMock();
+
+               $lb->expects( $this->once() )
+                       ->method( 'getConnectionRef' )
+                       ->with( DB_REPLICA, [ 'group1' ], 'someDbName' )
+                       ->will( $this->returnValue( $database ) );
+
+               $manager = new ConnectionManager( $lb, 'someDbName', [ 'group1' ] );
+               $actual = $manager->getReadConnectionRef();
+
+               $this->assertSame( $database, $actual );
+       }
+
+       public function testGetReadConnectionRef_withGroups() {
+               $database = $this->getIDatabaseMock();
+               $lb = $this->getLoadBalancerMock();
+
+               $lb->expects( $this->once() )
+                       ->method( 'getConnectionRef' )
+                       ->with( DB_REPLICA, [ 'group2' ], 'someDbName' )
+                       ->will( $this->returnValue( $database ) );
+
+               $manager = new ConnectionManager( $lb, 'someDbName', [ 'group1' ] );
+               $actual = $manager->getReadConnectionRef( [ 'group2' ] );
+
+               $this->assertSame( $database, $actual );
+       }
+
+       public function testGetWriteConnectionRef() {
+               $database = $this->getIDatabaseMock();
+               $lb = $this->getLoadBalancerMock();
+
+               $lb->expects( $this->once() )
+                       ->method( 'getConnectionRef' )
+                       ->with( DB_MASTER, [ 'group1' ], 'someDbName' )
+                       ->will( $this->returnValue( $database ) );
+
+               $manager = new ConnectionManager( $lb, 'someDbName', [ 'group1' ] );
+               $actual = $manager->getWriteConnectionRef();
+
+               $this->assertSame( $database, $actual );
+       }
+
+}
diff --git a/tests/phpunit/unit/includes/libs/rdbms/connectionmanager/SessionConsistentConnectionManagerTest.php b/tests/phpunit/unit/includes/libs/rdbms/connectionmanager/SessionConsistentConnectionManagerTest.php
new file mode 100644 (file)
index 0000000..8d7d104
--- /dev/null
@@ -0,0 +1,108 @@
+<?php
+
+namespace Wikimedia\Tests\Rdbms;
+
+use Wikimedia\Rdbms\IDatabase;
+use Wikimedia\Rdbms\LoadBalancer;
+use PHPUnit_Framework_MockObject_MockObject;
+use Wikimedia\Rdbms\SessionConsistentConnectionManager;
+
+/**
+ * @covers Wikimedia\Rdbms\SessionConsistentConnectionManager
+ *
+ * @author Daniel Kinzler
+ */
+class SessionConsistentConnectionManagerTest extends \PHPUnit\Framework\TestCase {
+
+       /**
+        * @return IDatabase|PHPUnit_Framework_MockObject_MockObject
+        */
+       private function getIDatabaseMock() {
+               return $this->getMockBuilder( IDatabase::class )
+                       ->getMock();
+       }
+
+       /**
+        * @return LoadBalancer|PHPUnit_Framework_MockObject_MockObject
+        */
+       private function getLoadBalancerMock() {
+               $lb = $this->getMockBuilder( LoadBalancer::class )
+                       ->disableOriginalConstructor()
+                       ->getMock();
+
+               return $lb;
+       }
+
+       public function testGetReadConnection() {
+               $database = $this->getIDatabaseMock();
+               $lb = $this->getLoadBalancerMock();
+
+               $lb->expects( $this->once() )
+                       ->method( 'getConnection' )
+                       ->with( DB_REPLICA )
+                       ->will( $this->returnValue( $database ) );
+
+               $manager = new SessionConsistentConnectionManager( $lb );
+               $actual = $manager->getReadConnection();
+
+               $this->assertSame( $database, $actual );
+       }
+
+       public function testGetReadConnectionReturnsWriteDbOnForceMatser() {
+               $database = $this->getIDatabaseMock();
+               $lb = $this->getLoadBalancerMock();
+
+               $lb->expects( $this->once() )
+                       ->method( 'getConnection' )
+                       ->with( DB_MASTER )
+                       ->will( $this->returnValue( $database ) );
+
+               $manager = new SessionConsistentConnectionManager( $lb );
+               $manager->prepareForUpdates();
+               $actual = $manager->getReadConnection();
+
+               $this->assertSame( $database, $actual );
+       }
+
+       public function testGetWriteConnection() {
+               $database = $this->getIDatabaseMock();
+               $lb = $this->getLoadBalancerMock();
+
+               $lb->expects( $this->once() )
+                       ->method( 'getConnection' )
+                       ->with( DB_MASTER )
+                       ->will( $this->returnValue( $database ) );
+
+               $manager = new SessionConsistentConnectionManager( $lb );
+               $actual = $manager->getWriteConnection();
+
+               $this->assertSame( $database, $actual );
+       }
+
+       public function testForceMaster() {
+               $database = $this->getIDatabaseMock();
+               $lb = $this->getLoadBalancerMock();
+
+               $lb->expects( $this->once() )
+                       ->method( 'getConnection' )
+                       ->with( DB_MASTER )
+                       ->will( $this->returnValue( $database ) );
+
+               $manager = new SessionConsistentConnectionManager( $lb );
+               $manager->prepareForUpdates();
+               $manager->getReadConnection();
+       }
+
+       public function testReleaseConnection() {
+               $database = $this->getIDatabaseMock();
+               $lb = $this->getLoadBalancerMock();
+
+               $lb->expects( $this->once() )
+                       ->method( 'reuseConnection' )
+                       ->with( $database )
+                       ->will( $this->returnValue( null ) );
+
+               $manager = new SessionConsistentConnectionManager( $lb );
+               $manager->releaseConnection( $database );
+       }
+}
diff --git a/tests/phpunit/unit/includes/libs/rdbms/database/DBConnRefTest.php b/tests/phpunit/unit/includes/libs/rdbms/database/DBConnRefTest.php
new file mode 100644 (file)
index 0000000..33e5c3b
--- /dev/null
@@ -0,0 +1,223 @@
+<?php
+
+use Wikimedia\Rdbms\Database;
+use Wikimedia\Rdbms\DBConnRef;
+use Wikimedia\Rdbms\FakeResultWrapper;
+use Wikimedia\Rdbms\IDatabase;
+use Wikimedia\Rdbms\ILoadBalancer;
+use Wikimedia\Rdbms\ResultWrapper;
+
+/**
+ * @covers Wikimedia\Rdbms\DBConnRef
+ */
+class DBConnRefTest extends PHPUnit\Framework\TestCase {
+
+       use MediaWikiCoversValidator;
+       use PHPUnit4And6Compat;
+
+       /**
+        * @return ILoadBalancer
+        */
+       private function getLoadBalancerMock() {
+               $lb = $this->getMock( ILoadBalancer::class );
+
+               $lb->method( 'getConnection' )->willReturnCallback(
+                       function () {
+                               return $this->getDatabaseMock();
+                       }
+               );
+
+               $lb->method( 'getConnectionRef' )->willReturnCallback(
+                       function () use ( $lb ) {
+                               return $this->getDBConnRef( $lb );
+                       }
+               );
+
+               return $lb;
+       }
+
+       /**
+        * @return IDatabase
+        */
+       private function getDatabaseMock() {
+               $db = $this->getMockBuilder( Database::class )
+                       ->disableOriginalConstructor()
+                       ->getMock();
+
+               $open = true;
+               $db->method( 'select' )->willReturnCallback( function () use ( &$open ) {
+                       if ( !$open ) {
+                               throw new LogicException( "Not open" );
+                       }
+
+                       return new FakeResultWrapper( [] );
+               } );
+               $db->method( 'close' )->willReturnCallback( function () use ( &$open ) {
+                       $open = false;
+
+                       return true;
+               } );
+               $db->method( 'isOpen' )->willReturnCallback( function () use ( &$open ) {
+                       return $open;
+               } );
+               $db->method( 'open' )->willReturnCallback( function () use ( &$open ) {
+                       $open = true;
+
+                       return $open;
+               } );
+               $db->method( '__toString' )->willReturn( 'MOCK_DB' );
+
+               return $db;
+       }
+
+       /**
+        * @return IDatabase
+        */
+       private function getDBConnRef( ILoadBalancer $lb = null ) {
+               $lb = $lb ?: $this->getLoadBalancerMock();
+               return new DBConnRef( $lb, $this->getDatabaseMock(), DB_MASTER );
+       }
+
+       public function testConstruct() {
+               $lb = $this->getLoadBalancerMock();
+               $ref = new DBConnRef( $lb, $this->getDatabaseMock(), DB_MASTER );
+
+               $this->assertInstanceOf( ResultWrapper::class, $ref->select( 'whatever', '*' ) );
+       }
+
+       public function testConstruct_params() {
+               $lb = $this->getMock( ILoadBalancer::class );
+
+               $lb->expects( $this->once() )
+                       ->method( 'getConnection' )
+                       ->with( DB_MASTER, [ 'test' ], 'dummy', ILoadBalancer::CONN_TRX_AUTOCOMMIT )
+                       ->willReturnCallback(
+                               function () {
+                                       return $this->getDatabaseMock();
+                               }
+                       );
+
+               $ref = new DBConnRef(
+                       $lb,
+                       [ DB_MASTER, [ 'test' ], 'dummy', ILoadBalancer::CONN_TRX_AUTOCOMMIT ],
+                       DB_MASTER
+               );
+
+               $this->assertInstanceOf( ResultWrapper::class, $ref->select( 'whatever', '*' ) );
+               $this->assertEquals( DB_MASTER, $ref->getReferenceRole() );
+
+               $ref2 = new DBConnRef(
+                       $lb,
+                       [ DB_MASTER, [ 'test' ], 'dummy', ILoadBalancer::CONN_TRX_AUTOCOMMIT ],
+                       DB_REPLICA
+               );
+               $this->assertEquals( DB_REPLICA, $ref2->getReferenceRole() );
+       }
+
+       public function testDestruct() {
+               $lb = $this->getLoadBalancerMock();
+
+               $lb->expects( $this->once() )
+                       ->method( 'reuseConnection' );
+
+               $this->innerMethodForTestDestruct( $lb );
+       }
+
+       private function innerMethodForTestDestruct( ILoadBalancer $lb ) {
+               $ref = $lb->getConnectionRef( DB_REPLICA );
+
+               $this->assertInstanceOf( ResultWrapper::class, $ref->select( 'whatever', '*' ) );
+       }
+
+       public function testConstruct_failure() {
+               $this->setExpectedException( InvalidArgumentException::class, '' );
+
+               $lb = $this->getLoadBalancerMock();
+               new DBConnRef( $lb, 17, DB_REPLICA ); // bad constructor argument
+       }
+
+       /**
+        * @covers Wikimedia\Rdbms\DBConnRef::getDomainId
+        */
+       public function testGetDomainID() {
+               $lb = $this->getMock( ILoadBalancer::class );
+
+               // getDomainID is optimized to not create a connection
+               $lb->expects( $this->never() )
+                       ->method( 'getConnection' );
+
+               $ref = new DBConnRef( $lb, [ DB_REPLICA, [], 'dummy', 0 ], DB_REPLICA );
+
+               $this->assertSame( 'dummy', $ref->getDomainID() );
+       }
+
+       /**
+        * @covers Wikimedia\Rdbms\DBConnRef::select
+        */
+       public function testSelect() {
+               // select should get passed through normally
+               $ref = $this->getDBConnRef();
+               $this->assertInstanceOf( ResultWrapper::class, $ref->select( 'whatever', '*' ) );
+       }
+
+       public function testToString() {
+               $ref = $this->getDBConnRef();
+               $this->assertInternalType( 'string', $ref->__toString() );
+
+               $lb = $this->getLoadBalancerMock();
+               $ref = new DBConnRef( $lb, [ DB_MASTER, [], 'test', 0 ], DB_MASTER );
+               $this->assertInternalType( 'string', $ref->__toString() );
+       }
+
+       /**
+        * @covers Wikimedia\Rdbms\DBConnRef::close
+        * @expectedException \Wikimedia\Rdbms\DBUnexpectedError
+        */
+       public function testClose() {
+               $lb = $this->getLoadBalancerMock();
+               $ref = new DBConnRef( $lb, [ DB_REPLICA, [], 'dummy', 0 ], DB_MASTER );
+               $ref->close();
+       }
+
+       /**
+        * @covers Wikimedia\Rdbms\DBConnRef::getReferenceRole
+        */
+       public function testGetReferenceRole() {
+               $lb = $this->getLoadBalancerMock();
+               $ref = new DBConnRef( $lb, [ DB_REPLICA, [], 'dummy', 0 ], DB_REPLICA );
+               $this->assertSame( DB_REPLICA, $ref->getReferenceRole() );
+
+               $ref = new DBConnRef( $lb, [ DB_MASTER, [], 'dummy', 0 ], DB_MASTER );
+               $this->assertSame( DB_MASTER, $ref->getReferenceRole() );
+
+               $ref = new DBConnRef( $lb, [ 1, [], 'dummy', 0 ], DB_REPLICA );
+               $this->assertSame( DB_REPLICA, $ref->getReferenceRole() );
+
+               $ref = new DBConnRef( $lb, [ 0, [], 'dummy', 0 ], DB_MASTER );
+               $this->assertSame( DB_MASTER, $ref->getReferenceRole() );
+       }
+
+       /**
+        * @covers Wikimedia\Rdbms\DBConnRef::getReferenceRole
+        * @expectedException Wikimedia\Rdbms\DBReadOnlyRoleError
+        * @dataProvider provideRoleExceptions
+        */
+       public function testRoleExceptions( $method, $args ) {
+               $lb = $this->getLoadBalancerMock();
+               $ref = new DBConnRef( $lb, [ DB_REPLICA, [], 'dummy', 0 ], DB_REPLICA );
+               $ref->$method( ...$args );
+       }
+
+       function provideRoleExceptions() {
+               return [
+                       [ 'insert', [ 'table', [ 'a' => 1 ] ] ],
+                       [ 'update', [ 'table', [ 'a' => 1 ], [ 'a' => 2 ] ] ],
+                       [ 'delete', [ 'table', [ 'a' => 1 ] ] ],
+                       [ 'replace', [ 'table', [ 'a' ], [ 'a' => 1 ] ] ],
+                       [ 'upsert', [ 'table', [ 'a' => 1 ], [ 'a' ], [ 'a = a + 1' ] ] ],
+                       [ 'lock', [ 'k', 'method' ] ],
+                       [ 'unlock', [ 'k', 'method' ] ],
+                       [ 'getScopedLockAndFlush', [ 'k', 'method', 1 ] ]
+               ];
+       }
+}
diff --git a/tests/phpunit/unit/includes/libs/rdbms/database/DatabaseDomainTest.php b/tests/phpunit/unit/includes/libs/rdbms/database/DatabaseDomainTest.php
new file mode 100644 (file)
index 0000000..b1d4fad
--- /dev/null
@@ -0,0 +1,226 @@
+<?php
+
+use Wikimedia\Rdbms\DatabaseDomain;
+
+/**
+ * @covers Wikimedia\Rdbms\DatabaseDomain
+ */
+class DatabaseDomainTest extends PHPUnit\Framework\TestCase {
+
+       use MediaWikiCoversValidator;
+       use PHPUnit4And6Compat;
+
+       public static function provideConstruct() {
+               return [
+                       'All strings' =>
+                               [ 'foo', 'bar', 'baz_', 'foo-bar-baz_' ],
+                       'Nothing' =>
+                               [ null, null, '', '' ],
+                       'Invalid $database' =>
+                               [ 0, 'bar', '', '', true ],
+                       'Invalid $schema' =>
+                               [ 'foo', 0, '', '', true ],
+                       'Invalid $prefix' =>
+                               [ 'foo', 'bar', 0, '', true ],
+                       'Dash' =>
+                               [ 'foo-bar', 'baz', 'baa_', 'foo?hbar-baz-baa_' ],
+                       'Question mark' =>
+                               [ 'foo?bar', 'baz', 'baa_', 'foo??bar-baz-baa_' ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideConstruct
+        */
+       public function testConstruct( $db, $schema, $prefix, $id, $exception = false ) {
+               if ( $exception ) {
+                       $this->setExpectedException( InvalidArgumentException::class );
+                       new DatabaseDomain( $db, $schema, $prefix );
+                       return;
+               }
+
+               $domain = new DatabaseDomain( $db, $schema, $prefix );
+               $this->assertInstanceOf( DatabaseDomain::class, $domain );
+               $this->assertEquals( $db, $domain->getDatabase() );
+               $this->assertEquals( $schema, $domain->getSchema() );
+               $this->assertEquals( $prefix, $domain->getTablePrefix() );
+               $this->assertEquals( $id, $domain->getId() );
+               $this->assertEquals( $id, strval( $domain ), 'toString' );
+       }
+
+       public static function provideNewFromId() {
+               return [
+                       'Basic' =>
+                               [ 'foo', 'foo', null, '' ],
+                       'db+prefix' =>
+                               [ 'foo-bar_', 'foo', null, 'bar_' ],
+                       'db+schema+prefix' =>
+                               [ 'foo-bar-baz_', 'foo', 'bar', 'baz_' ],
+                       '?h -> -' =>
+                               [ 'foo?hbar-baz-baa_', 'foo-bar', 'baz', 'baa_' ],
+                       '?? -> ?' =>
+                               [ 'foo??bar-baz-baa_', 'foo?bar', 'baz', 'baa_' ],
+                       '? is left alone' =>
+                               [ 'foo?bar-baz-baa_', 'foo?bar', 'baz', 'baa_' ],
+                       'too many parts' =>
+                               [ 'foo-bar-baz-baa_', '', '', '', true ],
+                       'from instance' =>
+                               [ DatabaseDomain::newUnspecified(), null, null, '' ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideNewFromId
+        */
+       public function testNewFromId( $id, $db, $schema, $prefix, $exception = false ) {
+               if ( $exception ) {
+                       $this->setExpectedException( InvalidArgumentException::class );
+                       DatabaseDomain::newFromId( $id );
+                       return;
+               }
+               $domain = DatabaseDomain::newFromId( $id );
+               $this->assertInstanceOf( DatabaseDomain::class, $domain );
+               $this->assertEquals( $db, $domain->getDatabase() );
+               $this->assertEquals( $schema, $domain->getSchema() );
+               $this->assertEquals( $prefix, $domain->getTablePrefix() );
+       }
+
+       public static function provideEquals() {
+               return [
+                       'Basic' =>
+                               [ 'foo', 'foo', null, '' ],
+                       'db+prefix' =>
+                               [ 'foo-bar_', 'foo', null, 'bar_' ],
+                       'db+schema+prefix' =>
+                               [ 'foo-bar-baz_', 'foo', 'bar', 'baz_' ],
+                       '?h -> -' =>
+                               [ 'foo?hbar-baz-baa_', 'foo-bar', 'baz', 'baa_' ],
+                       '?? -> ?' =>
+                               [ 'foo??bar-baz-baa_', 'foo?bar', 'baz', 'baa_' ],
+                       'Nothing' =>
+                               [ '', null, null, '' ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideEquals
+        * @covers Wikimedia\Rdbms\DatabaseDomain::equals
+        */
+       public function testEquals( $id, $db, $schema, $prefix ) {
+               $fromId = DatabaseDomain::newFromId( $id );
+               $this->assertInstanceOf( DatabaseDomain::class, $fromId );
+
+               $constructed = new DatabaseDomain( $db, $schema, $prefix );
+
+               $this->assertTrue( $constructed->equals( $id ), 'constructed equals string' );
+               $this->assertTrue( $fromId->equals( $id ), 'fromId equals string' );
+
+               $this->assertTrue( $constructed->equals( $fromId ), 'compare constructed to newId' );
+               $this->assertTrue( $fromId->equals( $constructed ), 'compare newId to constructed' );
+       }
+
+       /**
+        * @covers Wikimedia\Rdbms\DatabaseDomain::newUnspecified
+        */
+       public function testNewUnspecified() {
+               $domain = DatabaseDomain::newUnspecified();
+               $this->assertInstanceOf( DatabaseDomain::class, $domain );
+               $this->assertTrue( $domain->equals( '' ) );
+               $this->assertSame( null, $domain->getDatabase() );
+               $this->assertSame( null, $domain->getSchema() );
+               $this->assertSame( '', $domain->getTablePrefix() );
+       }
+
+       public static function provideIsCompatible() {
+               return [
+                       'Basic' =>
+                               [ 'foo', 'foo', null, '', true ],
+                       'db+prefix' =>
+                               [ 'foo-bar_', 'foo', null, 'bar_', true ],
+                       'db+schema+prefix' =>
+                               [ 'foo-bar-baz_', 'foo', 'bar', 'baz_', true ],
+                       'db+dontcare_schema+prefix' =>
+                               [ 'foo-bar-baz_', 'foo', null, 'baz_', false ],
+                       '?h -> -' =>
+                               [ 'foo?hbar-baz-baa_', 'foo-bar', 'baz', 'baa_', true ],
+                       '?? -> ?' =>
+                               [ 'foo??bar-baz-baa_', 'foo?bar', 'baz', 'baa_', true ],
+                       'Nothing' =>
+                               [ '', null, null, '', true ],
+                       'dontcaredb+dontcaredbschema+prefix' =>
+                               [ 'mywiki-mediawiki-prefix_', null, null, 'prefix_', false ],
+                       'db+dontcareschema+prefix' =>
+                               [ 'mywiki-schema-prefix_', 'mywiki', null, 'prefix_', false ],
+                       'postgres-db-jobqueue' =>
+                               [ 'postgres-mediawiki-', 'postgres', null, '', false ]
+               ];
+       }
+
+       /**
+        * @dataProvider provideIsCompatible
+        * @covers Wikimedia\Rdbms\DatabaseDomain::isCompatible
+        */
+       public function testIsCompatible( $id, $db, $schema, $prefix, $transitive ) {
+               $compareIdObj = DatabaseDomain::newFromId( $id );
+               $this->assertInstanceOf( DatabaseDomain::class, $compareIdObj );
+
+               $fromId = new DatabaseDomain( $db, $schema, $prefix );
+
+               $this->assertTrue( $fromId->isCompatible( $id ), 'constructed equals string' );
+               $this->assertTrue( $fromId->isCompatible( $compareIdObj ), 'fromId equals string' );
+
+               $this->assertEquals( $transitive, $compareIdObj->isCompatible( $fromId ),
+                       'test transitivity of nulls components' );
+       }
+
+       public static function provideIsCompatible2() {
+               return [
+                       'db+schema+prefix' =>
+                               [ 'mywiki-schema-prefix_', 'thatwiki', 'schema', 'prefix_' ],
+                       'dontcaredb+dontcaredbschema+prefix' =>
+                               [ 'thatwiki-mediawiki-otherprefix_', null, null, 'prefix_' ],
+                       'db+dontcareschema+prefix' =>
+                               [ 'notmywiki-schema-prefix_', 'mywiki', null, 'prefix_' ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideIsCompatible2
+        * @covers Wikimedia\Rdbms\DatabaseDomain::isCompatible
+        */
+       public function testIsCompatible2( $id, $db, $schema, $prefix ) {
+               $compareIdObj = DatabaseDomain::newFromId( $id );
+               $this->assertInstanceOf( DatabaseDomain::class, $compareIdObj );
+
+               $fromId = new DatabaseDomain( $db, $schema, $prefix );
+
+               $this->assertFalse( $fromId->isCompatible( $id ), 'constructed equals string' );
+               $this->assertFalse( $fromId->isCompatible( $compareIdObj ), 'fromId equals string' );
+       }
+
+       /**
+        * @expectedException InvalidArgumentException
+        */
+       public function testSchemaWithNoDB1() {
+               new DatabaseDomain( null, 'schema', '' );
+       }
+
+       /**
+        * @expectedException InvalidArgumentException
+        */
+       public function testSchemaWithNoDB2() {
+               DatabaseDomain::newFromId( '-schema-prefix' );
+       }
+
+       /**
+        * @covers Wikimedia\Rdbms\DatabaseDomain::isUnspecified
+        */
+       public function testIsUnspecified() {
+               $domain = new DatabaseDomain( null, null, '' );
+               $this->assertTrue( $domain->isUnspecified() );
+               $domain = new DatabaseDomain( 'mywiki', null, '' );
+               $this->assertFalse( $domain->isUnspecified() );
+               $domain = new DatabaseDomain( 'mywiki', null, '' );
+               $this->assertFalse( $domain->isUnspecified() );
+       }
+}
diff --git a/tests/phpunit/unit/includes/libs/rdbms/database/DatabaseMssqlTest.php b/tests/phpunit/unit/includes/libs/rdbms/database/DatabaseMssqlTest.php
new file mode 100644 (file)
index 0000000..414042d
--- /dev/null
@@ -0,0 +1,62 @@
+<?php
+
+use Wikimedia\Rdbms\Database;
+use Wikimedia\Rdbms\DatabaseMssql;
+
+class DatabaseMssqlTest extends PHPUnit\Framework\TestCase {
+
+       use MediaWikiCoversValidator;
+       use PHPUnit4And6Compat;
+
+       /**
+        * @return PHPUnit_Framework_MockObject_MockObject|DatabaseMssql
+        */
+       private function getMockDb() {
+               return $this->getMockBuilder( DatabaseMssql::class )
+                       ->disableOriginalConstructor()
+                       ->setMethods( null )
+                       ->getMock();
+       }
+
+       public function provideBuildSubstring() {
+               yield [ 'someField', 1, 2, 'SUBSTRING(someField,1,2)' ];
+               yield [ 'someField', 1, null, 'SUBSTRING(someField,1,2147483647)' ];
+               yield [ 'someField', 1, 3333333333, 'SUBSTRING(someField,1,3333333333)' ];
+       }
+
+       /**
+        * @covers Wikimedia\Rdbms\DatabaseMssql::buildSubstring
+        * @dataProvider provideBuildSubstring
+        */
+       public function testBuildSubstring( $input, $start, $length, $expected ) {
+               $mockDb = $this->getMockDb();
+               $output = $mockDb->buildSubstring( $input, $start, $length );
+               $this->assertSame( $expected, $output );
+       }
+
+       public function provideBuildSubstring_invalidParams() {
+               yield [ -1, 1 ];
+               yield [ 1, -1 ];
+               yield [ 1, 'foo' ];
+               yield [ 'foo', 1 ];
+               yield [ null, 1 ];
+               yield [ 0, 1 ];
+       }
+
+       /**
+        * @covers Wikimedia\Rdbms\DatabaseMssql::buildSubstring
+        * @dataProvider provideBuildSubstring_invalidParams
+        */
+       public function testBuildSubstring_invalidParams( $start, $length ) {
+               $mockDb = $this->getMockDb();
+               $this->setExpectedException( InvalidArgumentException::class );
+               $mockDb->buildSubstring( 'foo', $start, $length );
+       }
+
+       /**
+        * @covers \Wikimedia\Rdbms\DatabaseMssql::getAttributes
+        */
+       public function testAttributes() {
+               $this->assertTrue( DatabaseMssql::getAttributes()[Database::ATTR_SCHEMAS_AS_TABLE_GROUPS] );
+       }
+}
diff --git a/tests/phpunit/unit/includes/libs/rdbms/database/DatabaseMysqlBaseTest.php b/tests/phpunit/unit/includes/libs/rdbms/database/DatabaseMysqlBaseTest.php
new file mode 100644 (file)
index 0000000..4c92545
--- /dev/null
@@ -0,0 +1,740 @@
+<?php
+/**
+ * Holds tests for DatabaseMysqlBase class.
+ *
+ * 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; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * 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.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @author Antoine Musso
+ * @copyright © 2013 Antoine Musso
+ * @copyright © 2013 Wikimedia Foundation and contributors
+ */
+
+use Wikimedia\Rdbms\MySQLMasterPos;
+use Wikimedia\TestingAccessWrapper;
+
+class DatabaseMysqlBaseTest extends PHPUnit\Framework\TestCase {
+
+       use MediaWikiCoversValidator;
+       use PHPUnit4And6Compat;
+
+       /**
+        * @dataProvider provideDiapers
+        * @covers Wikimedia\Rdbms\DatabaseMysqlBase::addIdentifierQuotes
+        */
+       public function testAddIdentifierQuotes( $expected, $in ) {
+               $db = $this->getMockBuilder( DatabaseMysqli::class )
+                       ->disableOriginalConstructor()
+                       ->setMethods( null )
+                       ->getMock();
+
+               $quoted = $db->addIdentifierQuotes( $in );
+               $this->assertEquals( $expected, $quoted );
+       }
+
+       /**
+        * Feeds testAddIdentifierQuotes
+        *
+        * Named per T22281 convention.
+        */
+       public static function provideDiapers() {
+               return [
+                       // Format: expected, input
+                       [ '``', '' ],
+
+                       // Yeah I really hate loosely typed PHP idiocies nowadays
+                       [ '``', null ],
+
+                       // Dear codereviewer, guess what addIdentifierQuotes()
+                       // will return with thoses:
+                       [ '``', false ],
+                       [ '`1`', true ],
+
+                       // We never know what could happen
+                       [ '`0`', 0 ],
+                       [ '`1`', 1 ],
+
+                       // Whatchout! Should probably use something more meaningful
+                       [ "`'`", "'" ],  # single quote
+                       [ '`"`', '"' ],  # double quote
+                       [ '````', '`' ], # backtick
+                       [ '`’`', '’' ],  # apostrophe (look at your encyclopedia)
+
+                       // sneaky NUL bytes are lurking everywhere
+                       [ '``', "\0" ],
+                       [ '`xyzzy`', "\0x\0y\0z\0z\0y\0" ],
+
+                       // unicode chars
+                       [
+                               "`\u{0001}a\u{FFFF}b`",
+                               "\u{0001}a\u{FFFF}b"
+                       ],
+                       [
+                               "`\u{0001}\u{FFFF}`",
+                               "\u{0001}\u{0000}\u{FFFF}\u{0000}"
+                       ],
+                       [ '`☃`', '☃' ],
+                       [ '`メインページ`', 'メインページ' ],
+                       [ '`Басты_бет`', 'Басты_бет' ],
+
+                       // Real world:
+                       [ '`Alix`', 'Alix' ],  # while( ! $recovered ) { sleep(); }
+                       [ '`Backtick: ```', 'Backtick: `' ],
+                       [ '`This is a test`', 'This is a test' ],
+               ];
+       }
+
+       private function getMockForViews() {
+               $db = $this->getMockBuilder( DatabaseMysqli::class )
+                       ->disableOriginalConstructor()
+                       ->setMethods( [ 'fetchRow', 'query', 'getDBname' ] )
+                       ->getMock();
+
+               $db->method( 'query' )
+                       ->with( $this->anything() )
+                       ->willReturn( new FakeResultWrapper( [
+                               (object)[ 'Tables_in_' => 'view1' ],
+                               (object)[ 'Tables_in_' => 'view2' ],
+                               (object)[ 'Tables_in_' => 'myview' ]
+                       ] ) );
+               $db->method( 'getDBname' )->willReturn( '' );
+
+               return $db;
+       }
+
+       /**
+        * @covers Wikimedia\Rdbms\DatabaseMysqlBase::listViews
+        */
+       public function testListviews() {
+               $db = $this->getMockForViews();
+
+               $this->assertEquals( [ 'view1', 'view2', 'myview' ],
+                       $db->listViews() );
+
+               // Prefix filtering
+               $this->assertEquals( [ 'view1', 'view2' ],
+                       $db->listViews( 'view' ) );
+               $this->assertEquals( [ 'myview' ],
+                       $db->listViews( 'my' ) );
+               $this->assertEquals( [],
+                       $db->listViews( 'UNUSED_PREFIX' ) );
+               $this->assertEquals( [ 'view1', 'view2', 'myview' ],
+                       $db->listViews( '' ) );
+       }
+
+       /**
+        * @covers Wikimedia\Rdbms\MySQLMasterPos
+        */
+       public function testBinLogName() {
+               $pos = new MySQLMasterPos( "db1052.2424/4643", 1 );
+
+               $this->assertEquals( "db1052", $pos->getLogName() );
+               $this->assertEquals( "db1052.2424", $pos->getLogFile() );
+               $this->assertEquals( [ 2424, 4643 ], $pos->getLogPosition() );
+       }
+
+       /**
+        * @dataProvider provideComparePositions
+        * @covers Wikimedia\Rdbms\MySQLMasterPos
+        */
+       public function testHasReached(
+               MySQLMasterPos $lowerPos, MySQLMasterPos $higherPos, $match, $hetero
+       ) {
+               if ( $match ) {
+                       $this->assertTrue( $lowerPos->channelsMatch( $higherPos ) );
+
+                       if ( $hetero ) {
+                               // Each position is has one channel higher than the other
+                               $this->assertFalse( $higherPos->hasReached( $lowerPos ) );
+                       } else {
+                               $this->assertTrue( $higherPos->hasReached( $lowerPos ) );
+                       }
+                       $this->assertTrue( $lowerPos->hasReached( $lowerPos ) );
+                       $this->assertTrue( $higherPos->hasReached( $higherPos ) );
+                       $this->assertFalse( $lowerPos->hasReached( $higherPos ) );
+               } else { // channels don't match
+                       $this->assertFalse( $lowerPos->channelsMatch( $higherPos ) );
+
+                       $this->assertFalse( $higherPos->hasReached( $lowerPos ) );
+                       $this->assertFalse( $lowerPos->hasReached( $higherPos ) );
+               }
+       }
+
+       public static function provideComparePositions() {
+               $now = microtime( true );
+
+               return [
+                       // Binlog style
+                       [
+                               new MySQLMasterPos( 'db1034-bin.000976/843431247', $now ),
+                               new MySQLMasterPos( 'db1034-bin.000976/843431248', $now ),
+                               true,
+                               false
+                       ],
+                       [
+                               new MySQLMasterPos( 'db1034-bin.000976/999', $now ),
+                               new MySQLMasterPos( 'db1034-bin.000976/1000', $now ),
+                               true,
+                               false
+                       ],
+                       [
+                               new MySQLMasterPos( 'db1034-bin.000976/999', $now ),
+                               new MySQLMasterPos( 'db1035-bin.000976/1000', $now ),
+                               false,
+                               false
+                       ],
+                       // MySQL GTID style
+                       [
+                               new MySQLMasterPos( '3E11FA47-71CA-11E1-9E33-C80AA9429562:1-23', $now ),
+                               new MySQLMasterPos( '3E11FA47-71CA-11E1-9E33-C80AA9429562:5-24', $now ),
+                               true,
+                               false
+                       ],
+                       [
+                               new MySQLMasterPos( '3E11FA47-71CA-11E1-9E33-C80AA9429562:5-99', $now ),
+                               new MySQLMasterPos( '3E11FA47-71CA-11E1-9E33-C80AA9429562:1-100', $now ),
+                               true,
+                               false
+                       ],
+                       [
+                               new MySQLMasterPos( '3E11FA47-71CA-11E1-9E33-C80AA9429562:1-99', $now ),
+                               new MySQLMasterPos( '1E11FA47-71CA-11E1-9E33-C80AA9429562:1-100', $now ),
+                               false,
+                               false
+                       ],
+                       // MariaDB GTID style
+                       [
+                               new MySQLMasterPos( '255-11-23', $now ),
+                               new MySQLMasterPos( '255-11-24', $now ),
+                               true,
+                               false
+                       ],
+                       [
+                               new MySQLMasterPos( '255-11-99', $now ),
+                               new MySQLMasterPos( '255-11-100', $now ),
+                               true,
+                               false
+                       ],
+                       [
+                               new MySQLMasterPos( '255-11-999', $now ),
+                               new MySQLMasterPos( '254-11-1000', $now ),
+                               false,
+                               false
+                       ],
+                       [
+                               new MySQLMasterPos( '255-11-23,256-12-50', $now ),
+                               new MySQLMasterPos( '255-11-24', $now ),
+                               true,
+                               false
+                       ],
+                       [
+                               new MySQLMasterPos( '255-11-99,256-12-50,257-12-50', $now ),
+                               new MySQLMasterPos( '255-11-1000', $now ),
+                               true,
+                               false
+                       ],
+                       [
+                               new MySQLMasterPos( '255-11-23,256-12-50', $now ),
+                               new MySQLMasterPos( '255-11-24,155-52-63', $now ),
+                               true,
+                               false
+                       ],
+                       [
+                               new MySQLMasterPos( '255-11-99,256-12-50,257-12-50', $now ),
+                               new MySQLMasterPos( '255-11-1000,256-12-51', $now ),
+                               true,
+                               false
+                       ],
+                       [
+                               new MySQLMasterPos( '255-11-99,256-12-50', $now ),
+                               new MySQLMasterPos( '255-13-1000,256-14-49', $now ),
+                               true,
+                               true
+                       ],
+                       [
+                               new MySQLMasterPos( '253-11-999,255-11-999', $now ),
+                               new MySQLMasterPos( '254-11-1000', $now ),
+                               false,
+                               false
+                       ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideChannelPositions
+        * @covers Wikimedia\Rdbms\MySQLMasterPos
+        */
+       public function testChannelsMatch( MySQLMasterPos $pos1, MySQLMasterPos $pos2, $matches ) {
+               $this->assertEquals( $matches, $pos1->channelsMatch( $pos2 ) );
+               $this->assertEquals( $matches, $pos2->channelsMatch( $pos1 ) );
+
+               $roundtripPos = new MySQLMasterPos( (string)$pos1, 1 );
+               $this->assertEquals( (string)$pos1, (string)$roundtripPos );
+       }
+
+       public static function provideChannelPositions() {
+               $now = microtime( true );
+
+               return [
+                       [
+                               new MySQLMasterPos( 'db1034-bin.000876/44', $now ),
+                               new MySQLMasterPos( 'db1034-bin.000976/74', $now ),
+                               true
+                       ],
+                       [
+                               new MySQLMasterPos( 'db1052-bin.000976/999', $now ),
+                               new MySQLMasterPos( 'db1052-bin.000976/1000', $now ),
+                               true
+                       ],
+                       [
+                               new MySQLMasterPos( 'db1066-bin.000976/9999', $now ),
+                               new MySQLMasterPos( 'db1035-bin.000976/10000', $now ),
+                               false
+                       ],
+                       [
+                               new MySQLMasterPos( 'db1066-bin.000976/9999', $now ),
+                               new MySQLMasterPos( 'trump2016.000976/10000', $now ),
+                               false
+                       ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideCommonDomainGTIDs
+        * @covers Wikimedia\Rdbms\MySQLMasterPos
+        */
+       public function testCommonGtidDomains( MySQLMasterPos $pos, MySQLMasterPos $ref, $gtids ) {
+               $this->assertEquals( $gtids, MySQLMasterPos::getCommonDomainGTIDs( $pos, $ref ) );
+       }
+
+       public static function provideCommonDomainGTIDs() {
+               return [
+                       [
+                               new MySQLMasterPos( '255-13-99,256-12-50,257-14-50', 1 ),
+                               new MySQLMasterPos( '255-11-1000', 1 ),
+                               [ '255-13-99' ]
+                       ],
+                       [
+                               new MySQLMasterPos(
+                                       '2E11FA47-71CA-11E1-9E33-C80AA9429562:1-5,' .
+                                       '3E11FA47-71CA-11E1-9E33-C80AA9429562:20-99,' .
+                                       '7E11FA47-71CA-11E1-9E33-C80AA9429562:1-30',
+                                       1
+                               ),
+                               new MySQLMasterPos(
+                                       '1E11FA47-71CA-11E1-9E33-C80AA9429562:30-100,' .
+                                       '3E11FA47-71CA-11E1-9E33-C80AA9429562:30-66',
+                                       1
+                               ),
+                               [ '3E11FA47-71CA-11E1-9E33-C80AA9429562:20-99' ]
+                       ]
+               ];
+       }
+
+       /**
+        * @dataProvider provideLagAmounts
+        * @covers Wikimedia\Rdbms\DatabaseMysqlBase::getLag
+        * @covers Wikimedia\Rdbms\DatabaseMysqlBase::getLagFromPtHeartbeat
+        */
+       public function testPtHeartbeat( $lag ) {
+               $db = $this->getMockBuilder( DatabaseMysqli::class )
+                       ->disableOriginalConstructor()
+                       ->setMethods( [
+                               'getLagDetectionMethod', 'getHeartbeatData', 'getMasterServerInfo' ] )
+                       ->getMock();
+
+               $db->method( 'getLagDetectionMethod' )
+                       ->willReturn( 'pt-heartbeat' );
+
+               $db->method( 'getMasterServerInfo' )
+                       ->willReturn( [ 'serverId' => 172, 'asOf' => time() ] );
+
+               // Fake the current time.
+               list( $nowSecFrac, $nowSec ) = explode( ' ', microtime() );
+               $now = (float)$nowSec + (float)$nowSecFrac;
+               // Fake the heartbeat time.
+               // Work arounds for weak DataTime microseconds support.
+               $ptTime = $now - $lag;
+               $ptSec = (int)$ptTime;
+               $ptSecFrac = ( $ptTime - $ptSec );
+               $ptDateTime = new DateTime( "@$ptSec" );
+               $ptTimeISO = $ptDateTime->format( 'Y-m-d\TH:i:s' );
+               $ptTimeISO .= ltrim( number_format( $ptSecFrac, 6 ), '0' );
+
+               $db->method( 'getHeartbeatData' )
+                       ->with( [ 'server_id' => 172 ] )
+                       ->willReturn( [ $ptTimeISO, $now ] );
+
+               $db->setLBInfo( 'clusterMasterHost', 'db1052' );
+               $lagEst = $db->getLag();
+
+               $this->assertGreaterThan( $lag - 0.010, $lagEst, "Correct heatbeat lag" );
+               $this->assertLessThan( $lag + 0.010, $lagEst, "Correct heatbeat lag" );
+       }
+
+       public static function provideLagAmounts() {
+               return [
+                       [ 0 ],
+                       [ 0.3 ],
+                       [ 6.5 ],
+                       [ 10.1 ],
+                       [ 200.2 ],
+                       [ 400.7 ],
+                       [ 600.22 ],
+                       [ 1000.77 ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideGtidData
+        * @covers Wikimedia\Rdbms\MySQLMasterPos
+        * @covers Wikimedia\Rdbms\DatabaseMysqlBase::getReplicaPos
+        * @covers Wikimedia\Rdbms\DatabaseMysqlBase::getMasterPos
+        */
+       public function testServerGtidTable( $gtable, $rBLtable, $mBLtable, $rGTIDs, $mGTIDs ) {
+               $db = $this->getMockBuilder( DatabaseMysqli::class )
+                       ->disableOriginalConstructor()
+                       ->setMethods( [
+                               'useGTIDs',
+                               'getServerGTIDs',
+                               'getServerRoleStatus',
+                               'getServerId',
+                               'getServerUUID'
+                       ] )
+                       ->getMock();
+
+               $db->method( 'useGTIDs' )->willReturn( true );
+               $db->method( 'getServerGTIDs' )->willReturn( $gtable );
+               $db->method( 'getServerRoleStatus' )->willReturnCallback(
+                       function ( $role ) use ( $rBLtable, $mBLtable ) {
+                               if ( $role === 'SLAVE' ) {
+                                       return $rBLtable;
+                               } elseif ( $role === 'MASTER' ) {
+                                       return $mBLtable;
+                               }
+
+                               return null;
+                       }
+               );
+               $db->method( 'getServerId' )->willReturn( 1 );
+               $db->method( 'getServerUUID' )->willReturn( '2E11FA47-71CA-11E1-9E33-C80AA9429562' );
+
+               if ( is_array( $rGTIDs ) ) {
+                       $this->assertEquals( $rGTIDs, $db->getReplicaPos()->getGTIDs() );
+               } else {
+                       $this->assertEquals( false, $db->getReplicaPos() );
+               }
+               if ( is_array( $mGTIDs ) ) {
+                       $this->assertEquals( $mGTIDs, $db->getMasterPos()->getGTIDs() );
+               } else {
+                       $this->assertEquals( false, $db->getMasterPos() );
+               }
+       }
+
+       public static function provideGtidData() {
+               return [
+                       // MariaDB
+                       [
+                               [
+                                       'gtid_domain_id' => 100,
+                                       'gtid_current_pos' => '100-13-77',
+                                       'gtid_binlog_pos' => '100-13-77',
+                                       'gtid_slave_pos' => null // master
+                               ],
+                               [
+                                       'Relay_Master_Log_File' => 'host.1600',
+                                       'Exec_Master_Log_Pos' => '77'
+                               ],
+                               [
+                                       'File' => 'host.1600',
+                                       'Position' => '77'
+                               ],
+                               [],
+                               [ '100' => '100-13-77' ]
+                       ],
+                       [
+                               [
+                                       'gtid_domain_id' => 100,
+                                       'gtid_current_pos' => '100-13-77',
+                                       'gtid_binlog_pos' => '100-13-77',
+                                       'gtid_slave_pos' => '100-13-77' // replica
+                               ],
+                               [
+                                       'Relay_Master_Log_File' => 'host.1600',
+                                       'Exec_Master_Log_Pos' => '77'
+                               ],
+                               [],
+                               [ '100' => '100-13-77' ],
+                               [ '100' => '100-13-77' ]
+                       ],
+                       [
+                               [
+                                       'gtid_current_pos' => '100-13-77',
+                                       'gtid_binlog_pos' => '100-13-77',
+                                       'gtid_slave_pos' => '100-13-77' // replica
+                               ],
+                               [
+                                       'Relay_Master_Log_File' => 'host.1600',
+                                       'Exec_Master_Log_Pos' => '77'
+                               ],
+                               [],
+                               [ '100' => '100-13-77' ],
+                               [ '100' => '100-13-77' ]
+                       ],
+                       // MySQL
+                       [
+                               [
+                                       'gtid_executed' => '2E11FA47-71CA-11E1-9E33-C80AA9429562:1-77'
+                               ],
+                               [
+                                       'Relay_Master_Log_File' => 'host.1600',
+                                       'Exec_Master_Log_Pos' => '77'
+                               ],
+                               [], // only a replica
+                               [ '2E11FA47-71CA-11E1-9E33-C80AA9429562'
+                                       => '2E11FA47-71CA-11E1-9E33-C80AA9429562:1-77' ],
+                               // replica/master use same var
+                               [ '2E11FA47-71CA-11E1-9E33-C80AA9429562'
+                                       => '2E11FA47-71CA-11E1-9E33-C80AA9429562:1-77' ],
+                       ],
+                       [
+                               [
+                                       'gtid_executed' => '2E11FA47-71CA-11E1-9E33-C80AA9429562:1-49,' .
+                                               '2E11FA47-71CA-11E1-9E33-C80AA9429562:51-77'
+                               ],
+                               [
+                                       'Relay_Master_Log_File' => 'host.1600',
+                                       'Exec_Master_Log_Pos' => '77'
+                               ],
+                               [], // only a replica
+                               [ '2E11FA47-71CA-11E1-9E33-C80AA9429562'
+                                       => '2E11FA47-71CA-11E1-9E33-C80AA9429562:51-77' ],
+                               // replica/master use same var
+                               [ '2E11FA47-71CA-11E1-9E33-C80AA9429562'
+                                       => '2E11FA47-71CA-11E1-9E33-C80AA9429562:51-77' ],
+                       ],
+                       [
+                               [
+                                       'gtid_executed' => null, // not enabled?
+                                       'gtid_binlog_pos' => null
+                               ],
+                               [
+                                       'Relay_Master_Log_File' => 'host.1600',
+                                       'Exec_Master_Log_Pos' => '77'
+                               ],
+                               [], // only a replica
+                               [], // binlog fallback
+                               false
+                       ],
+                       [
+                               [
+                                       'gtid_executed' => null, // not enabled?
+                                       'gtid_binlog_pos' => null
+                               ],
+                               [], // no replication
+                               [], // no replication
+                               false,
+                               false
+                       ]
+               ];
+       }
+
+       /**
+        * @covers Wikimedia\Rdbms\MySQLMasterPos
+        */
+       public function testSerialize() {
+               $pos = new MySQLMasterPos( '3E11FA47-71CA-11E1-9E33-C80AA9429562:99', 53636363 );
+               $roundtripPos = unserialize( serialize( $pos ) );
+
+               $this->assertEquals( $pos, $roundtripPos );
+
+               $pos = new MySQLMasterPos( '255-11-23', 53636363 );
+               $roundtripPos = unserialize( serialize( $pos ) );
+
+               $this->assertEquals( $pos, $roundtripPos );
+       }
+
+       /**
+        * @covers Wikimedia\Rdbms\DatabaseMysqlBase::isInsertSelectSafe
+        * @dataProvider provideInsertSelectCases
+        */
+       public function testInsertSelectIsSafe( $insertOpts, $selectOpts, $row, $safe ) {
+               $db = $this->getMockBuilder( DatabaseMysqli::class )
+                       ->disableOriginalConstructor()
+                       ->setMethods( [ 'getReplicationSafetyInfo' ] )
+                       ->getMock();
+               $db->method( 'getReplicationSafetyInfo' )->willReturn( (object)$row );
+               $dbw = TestingAccessWrapper::newFromObject( $db );
+
+               $this->assertEquals( $safe, $dbw->isInsertSelectSafe( $insertOpts, $selectOpts ) );
+       }
+
+       public function provideInsertSelectCases() {
+               return [
+                       [
+                               [],
+                               [],
+                               [
+                                       'innodb_autoinc_lock_mode' => '2',
+                                       'binlog_format' => 'ROW',
+                               ],
+                               true
+                       ],
+                       [
+                               [],
+                               [ 'LIMIT' => 100 ],
+                               [
+                                       'innodb_autoinc_lock_mode' => '2',
+                                       'binlog_format' => 'ROW',
+                               ],
+                               true
+                       ],
+                       [
+                               [],
+                               [ 'LIMIT' => 100 ],
+                               [
+                                       'innodb_autoinc_lock_mode' => '0',
+                                       'binlog_format' => 'STATEMENT',
+                               ],
+                               false
+                       ],
+                       [
+                               [],
+                               [],
+                               [
+                                       'innodb_autoinc_lock_mode' => '2',
+                                       'binlog_format' => 'STATEMENT',
+                               ],
+                               false
+                       ],
+                       [
+                               [ 'NO_AUTO_COLUMNS' ],
+                               [ 'LIMIT' => 100 ],
+                               [
+                                       'innodb_autoinc_lock_mode' => '0',
+                                       'binlog_format' => 'STATEMENT',
+                               ],
+                               false
+                       ],
+                       [
+                               [],
+                               [],
+                               [
+                                       'innodb_autoinc_lock_mode' => 0,
+                                       'binlog_format' => 'STATEMENT',
+                               ],
+                               true
+                       ],
+                       [
+                               [ 'NO_AUTO_COLUMNS' ],
+                               [],
+                               [
+                                       'innodb_autoinc_lock_mode' => 2,
+                                       'binlog_format' => 'STATEMENT',
+                               ],
+                               true
+                       ],
+                       [
+                               [ 'NO_AUTO_COLUMNS' ],
+                               [],
+                               [
+                                       'innodb_autoinc_lock_mode' => 0,
+                                       'binlog_format' => 'STATEMENT',
+                               ],
+                               true
+                       ],
+
+               ];
+       }
+
+       /**
+        * @covers \Wikimedia\Rdbms\DatabaseMysqlBase::buildIntegerCast
+        */
+       public function testBuildIntegerCast() {
+               $db = $this->getMockBuilder( DatabaseMysqli::class )
+                       ->disableOriginalConstructor()
+                       ->setMethods( null )
+                       ->getMock();
+               $output = $db->buildIntegerCast( 'fieldName' );
+               $this->assertSame( 'CAST( fieldName AS SIGNED )', $output );
+       }
+
+       /**
+        * @covers Wikimedia\Rdbms\Database::setIndexAliases
+        */
+       public function testIndexAliases() {
+               $db = $this->getMockBuilder( DatabaseMysqli::class )
+                       ->disableOriginalConstructor()
+                       ->setMethods( [ 'mysqlRealEscapeString', 'dbSchema', 'tablePrefix' ] )
+                       ->getMock();
+               $db->method( 'mysqlRealEscapeString' )->willReturnCallback(
+                       function ( $s ) {
+                               return str_replace( "'", "\\'", $s );
+                       }
+               );
+
+               $db->setIndexAliases( [ 'a_b_idx' => 'a_c_idx' ] );
+               $sql = $db->selectSQLText(
+                       'zend', 'field', [ 'a' => 'x' ], __METHOD__, [ 'USE INDEX' => 'a_b_idx' ] );
+
+               $this->assertEquals(
+                       "SELECT  field  FROM `zend`  FORCE INDEX (a_c_idx)  WHERE a = 'x'  ",
+                       $sql
+               );
+
+               $db->setIndexAliases( [] );
+               $sql = $db->selectSQLText(
+                       'zend', 'field', [ 'a' => 'x' ], __METHOD__, [ 'USE INDEX' => 'a_b_idx' ] );
+
+               $this->assertEquals(
+                       "SELECT  field  FROM `zend`  FORCE INDEX (a_b_idx)  WHERE a = 'x'  ",
+                       $sql
+               );
+       }
+
+       /**
+        * @covers Wikimedia\Rdbms\Database::setTableAliases
+        */
+       public function testTableAliases() {
+               $db = $this->getMockBuilder( DatabaseMysqli::class )
+                       ->disableOriginalConstructor()
+                       ->setMethods( [ 'mysqlRealEscapeString', 'dbSchema', 'tablePrefix' ] )
+                       ->getMock();
+               $db->method( 'mysqlRealEscapeString' )->willReturnCallback(
+                       function ( $s ) {
+                               return str_replace( "'", "\\'", $s );
+                       }
+               );
+
+               $db->setTableAliases( [
+                       'meow' => [ 'dbname' => 'feline', 'schema' => null, 'prefix' => 'cat_' ]
+               ] );
+               $sql = $db->selectSQLText( 'meow', 'field', [ 'a' => 'x' ], __METHOD__ );
+
+               $this->assertEquals(
+                       "SELECT  field  FROM `feline`.`cat_meow`    WHERE a = 'x'  ",
+                       $sql
+               );
+
+               $db->setTableAliases( [] );
+               $sql = $db->selectSQLText( 'meow', 'field', [ 'a' => 'x' ], __METHOD__ );
+
+               $this->assertEquals(
+                       "SELECT  field  FROM `meow`    WHERE a = 'x'  ",
+                       $sql
+               );
+       }
+}
diff --git a/tests/phpunit/unit/includes/libs/rdbms/database/DatabaseSQLTest.php b/tests/phpunit/unit/includes/libs/rdbms/database/DatabaseSQLTest.php
new file mode 100644 (file)
index 0000000..0e133d8
--- /dev/null
@@ -0,0 +1,2164 @@
+<?php
+
+use Wikimedia\Rdbms\IDatabase;
+use Wikimedia\Rdbms\LikeMatch;
+use Wikimedia\Rdbms\Database;
+use Wikimedia\TestingAccessWrapper;
+use Wikimedia\Rdbms\DBTransactionStateError;
+use Wikimedia\Rdbms\DBUnexpectedError;
+use Wikimedia\Rdbms\DBTransactionError;
+
+/**
+ * Test the parts of the Database abstract class that deal
+ * with creating SQL text.
+ */
+class DatabaseSQLTest extends PHPUnit\Framework\TestCase {
+
+       use MediaWikiCoversValidator;
+       use PHPUnit4And6Compat;
+
+       /** @var DatabaseTestHelper|Database */
+       private $database;
+
+       protected function setUp() {
+               parent::setUp();
+               $this->database = new DatabaseTestHelper( __CLASS__, [ 'cliMode' => true ] );
+       }
+
+       protected function assertLastSql( $sqlText ) {
+               $this->assertEquals(
+                       $sqlText,
+                       $this->database->getLastSqls()
+               );
+       }
+
+       protected function assertLastSqlDb( $sqlText, DatabaseTestHelper $db ) {
+               $this->assertEquals( $sqlText, $db->getLastSqls() );
+       }
+
+       /**
+        * @dataProvider provideSelect
+        * @covers Wikimedia\Rdbms\Database::select
+        * @covers Wikimedia\Rdbms\Database::selectSQLText
+        * @covers Wikimedia\Rdbms\Database::tableNamesWithIndexClauseOrJOIN
+        * @covers Wikimedia\Rdbms\Database::useIndexClause
+        * @covers Wikimedia\Rdbms\Database::ignoreIndexClause
+        * @covers Wikimedia\Rdbms\Database::makeSelectOptions
+        * @covers Wikimedia\Rdbms\Database::makeOrderBy
+        * @covers Wikimedia\Rdbms\Database::makeGroupByWithHaving
+        * @covers Wikimedia\Rdbms\Database::selectFieldsOrOptionsAggregate
+        * @covers Wikimedia\Rdbms\Database::selectOptionsIncludeLocking
+        */
+       public function testSelect( $sql, $sqlText ) {
+               $this->database->select(
+                       $sql['tables'],
+                       $sql['fields'],
+                       $sql['conds'] ?? [],
+                       __METHOD__,
+                       $sql['options'] ?? [],
+                       $sql['join_conds'] ?? []
+               );
+               $this->assertLastSql( $sqlText );
+       }
+
+       public static function provideSelect() {
+               return [
+                       [
+                               [
+                                       'tables' => 'table',
+                                       'fields' => [ 'field', 'alias' => 'field2' ],
+                                       'conds' => [ 'alias' => 'text' ],
+                               ],
+                               "SELECT field,field2 AS alias " .
+                                       "FROM table " .
+                                       "WHERE alias = 'text'"
+                       ],
+                       [
+                               [
+                                       'tables' => 'table',
+                                       'fields' => [ 'field', 'alias' => 'field2' ],
+                                       'conds' => 'alias = \'text\'',
+                               ],
+                               "SELECT field,field2 AS alias " .
+                               "FROM table " .
+                               "WHERE alias = 'text'"
+                       ],
+                       [
+                               [
+                                       'tables' => 'table',
+                                       'fields' => [ 'field', 'alias' => 'field2' ],
+                                       'conds' => [],
+                               ],
+                               "SELECT field,field2 AS alias " .
+                               "FROM table"
+                       ],
+                       [
+                               [
+                                       'tables' => 'table',
+                                       'fields' => [ 'field', 'alias' => 'field2' ],
+                                       'conds' => '',
+                               ],
+                               "SELECT field,field2 AS alias " .
+                               "FROM table"
+                       ],
+                       [
+                               [
+                                       'tables' => 'table',
+                                       'fields' => [ 'field', 'alias' => 'field2' ],
+                                       'conds' => '0', // T188314
+                               ],
+                               "SELECT field,field2 AS alias " .
+                               "FROM table " .
+                               "WHERE 0"
+                       ],
+                       [
+                               [
+                                       // 'tables' with space prepended indicates pre-escaped table name
+                                       'tables' => ' table LEFT JOIN table2',
+                                       'fields' => [ 'field' ],
+                                       'conds' => [ 'field' => 'text' ],
+                               ],
+                               "SELECT field FROM  table LEFT JOIN table2 WHERE field = 'text'"
+                       ],
+                       [
+                               [
+                                       // Empty 'tables' is allowed
+                                       'tables' => '',
+                                       'fields' => [ 'SPECIAL_QUERY()' ],
+                               ],
+                               "SELECT SPECIAL_QUERY()"
+                       ],
+                       [
+                               [
+                                       'tables' => 'table',
+                                       'fields' => [ 'field', 'alias' => 'field2' ],
+                                       'conds' => [ 'alias' => 'text' ],
+                                       'options' => [ 'LIMIT' => 1, 'ORDER BY' => 'field' ],
+                               ],
+                               "SELECT field,field2 AS alias " .
+                                       "FROM table " .
+                                       "WHERE alias = 'text' " .
+                                       "ORDER BY field " .
+                                       "LIMIT 1"
+                       ],
+                       [
+                               [
+                                       'tables' => [ 'table', 't2' => 'table2' ],
+                                       'fields' => [ 'tid', 'field', 'alias' => 'field2', 't2.id' ],
+                                       'conds' => [ 'alias' => 'text' ],
+                                       'options' => [ 'LIMIT' => 1, 'ORDER BY' => 'field' ],
+                                       'join_conds' => [ 't2' => [
+                                               'LEFT JOIN', 'tid = t2.id'
+                                       ] ],
+                               ],
+                               "SELECT tid,field,field2 AS alias,t2.id " .
+                                       "FROM table LEFT JOIN table2 t2 ON ((tid = t2.id)) " .
+                                       "WHERE alias = 'text' " .
+                                       "ORDER BY field " .
+                                       "LIMIT 1"
+                       ],
+                       [
+                               [
+                                       'tables' => [ 'table', 't2' => 'table2' ],
+                                       'fields' => [ 'tid', 'field', 'alias' => 'field2', 't2.id' ],
+                                       'conds' => [ 'alias' => 'text' ],
+                                       'options' => [ 'LIMIT' => 1, 'GROUP BY' => 'field', 'HAVING' => 'COUNT(*) > 1' ],
+                                       'join_conds' => [ 't2' => [
+                                               'LEFT JOIN', 'tid = t2.id'
+                                       ] ],
+                               ],
+                               "SELECT tid,field,field2 AS alias,t2.id " .
+                                       "FROM table LEFT JOIN table2 t2 ON ((tid = t2.id)) " .
+                                       "WHERE alias = 'text' " .
+                                       "GROUP BY field HAVING COUNT(*) > 1 " .
+                                       "LIMIT 1"
+                       ],
+                       [
+                               [
+                                       'tables' => [ 'table', 't2' => 'table2' ],
+                                       'fields' => [ 'tid', 'field', 'alias' => 'field2', 't2.id' ],
+                                       'conds' => [ 'alias' => 'text' ],
+                                       'options' => [
+                                               'LIMIT' => 1,
+                                               'GROUP BY' => [ 'field', 'field2' ],
+                                               'HAVING' => [ 'COUNT(*) > 1', 'field' => 1 ]
+                                       ],
+                                       'join_conds' => [ 't2' => [
+                                               'LEFT JOIN', 'tid = t2.id'
+                                       ] ],
+                               ],
+                               "SELECT tid,field,field2 AS alias,t2.id " .
+                                       "FROM table LEFT JOIN table2 t2 ON ((tid = t2.id)) " .
+                                       "WHERE alias = 'text' " .
+                                       "GROUP BY field,field2 HAVING (COUNT(*) > 1) AND field = '1' " .
+                                       "LIMIT 1"
+                       ],
+                       [
+                               [
+                                       'tables' => [ 'table' ],
+                                       'fields' => [ 'alias' => 'field' ],
+                                       'conds' => [ 'alias' => [ 1, 2, 3, 4 ] ],
+                               ],
+                               "SELECT field AS alias " .
+                                       "FROM table " .
+                                       "WHERE alias IN ('1','2','3','4')"
+                       ],
+                       [
+                               [
+                                       'tables' => 'table',
+                                       'fields' => [ 'field' ],
+                                       'options' => [ 'USE INDEX' => [ 'table' => 'X' ] ],
+                               ],
+                               // No-op by default
+                               "SELECT field FROM table"
+                       ],
+                       [
+                               [
+                                       'tables' => 'table',
+                                       'fields' => [ 'field' ],
+                                       'options' => [ 'IGNORE INDEX' => [ 'table' => 'X' ] ],
+                               ],
+                               // No-op by default
+                               "SELECT field FROM table"
+                       ],
+                       [
+                               [
+                                       'tables' => 'table',
+                                       'fields' => [ 'field' ],
+                                       'options' => [ 'DISTINCT' ],
+                               ],
+                               "SELECT DISTINCT field FROM table"
+                       ],
+                       [
+                               [
+                                       'tables' => 'table',
+                                       'fields' => [ 'field' ],
+                                       'options' => [ 'LOCK IN SHARE MODE' ],
+                               ],
+                               "SELECT field FROM table      LOCK IN SHARE MODE"
+                       ],
+                       [
+                               [
+                                       'tables' => 'table',
+                                       'fields' => [ 'field' ],
+                                       'options' => [ 'EXPLAIN' => true ],
+                               ],
+                               'EXPLAIN SELECT field FROM table'
+                       ],
+                       [
+                               [
+                                       'tables' => 'table',
+                                       'fields' => [ 'field' ],
+                                       'options' => [ 'FOR UPDATE' ],
+                               ],
+                               "SELECT field FROM table      FOR UPDATE"
+                       ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideLockForUpdate
+        * @covers Wikimedia\Rdbms\Database::lockForUpdate
+        */
+       public function testLockForUpdate( $sql, $sqlText ) {
+               $this->database->startAtomic( __METHOD__ );
+               $this->database->lockForUpdate(
+                       $sql['tables'],
+                       $sql['conds'] ?? [],
+                       __METHOD__,
+                       $sql['options'] ?? [],
+                       $sql['join_conds'] ?? []
+               );
+               $this->database->endAtomic( __METHOD__ );
+
+               $this->assertLastSql( "BEGIN; $sqlText; COMMIT" );
+       }
+
+       public static function provideLockForUpdate() {
+               return [
+                       [
+                               [
+                                       'tables' => [ 'table' ],
+                                       'conds' => [ 'field' => [ 1, 2, 3, 4 ] ],
+                               ],
+                               "SELECT COUNT(*) AS rowcount FROM " .
+                               "(SELECT 1 FROM table WHERE field IN ('1','2','3','4')    " .
+                               "FOR UPDATE) tmp_count"
+                       ],
+                       [
+                               [
+                                       'tables' => [ 'table', 't2' => 'table2' ],
+                                       'conds' => [ 'field' => 'text' ],
+                                       'options' => [ 'LIMIT' => 1, 'ORDER BY' => 'field' ],
+                                       'join_conds' => [ 't2' => [
+                                               'LEFT JOIN', 'tid = t2.id'
+                                       ] ],
+                               ],
+                               "SELECT COUNT(*) AS rowcount FROM " .
+                               "(SELECT 1 FROM table LEFT JOIN table2 t2 ON ((tid = t2.id)) " .
+                               "WHERE field = 'text' ORDER BY field LIMIT 1   FOR UPDATE) tmp_count"
+                       ],
+                       [
+                               [
+                                       'tables' => 'table',
+                               ],
+                               "SELECT COUNT(*) AS rowcount FROM " .
+                               "(SELECT 1 FROM table      FOR UPDATE) tmp_count"
+                       ],
+               ];
+       }
+
+       /**
+        * @covers Wikimedia\Rdbms\Subquery
+        * @dataProvider provideSelectRowCount
+        * @param array $sql
+        * @param string $sqlText
+        */
+       public function testSelectRowCount( $sql, $sqlText ) {
+               $this->database->selectRowCount(
+                       $sql['tables'],
+                       $sql['field'],
+                       $sql['conds'] ?? [],
+                       __METHOD__,
+                       $sql['options'] ?? [],
+                       $sql['join_conds'] ?? []
+               );
+               $this->assertLastSql( $sqlText );
+       }
+
+       public static function provideSelectRowCount() {
+               return [
+                       [
+                               [
+                                       'tables' => 'table',
+                                       'field' => [ '*' ],
+                                       'conds' => [ 'field' => 'text' ],
+                               ],
+                               "SELECT COUNT(*) AS rowcount FROM " .
+                               "(SELECT 1 FROM table WHERE field = 'text'  ) tmp_count"
+                       ],
+                       [
+                               [
+                                       'tables' => 'table',
+                                       'field' => [ 'column' ],
+                                       'conds' => [ 'field' => 'text' ],
+                               ],
+                               "SELECT COUNT(*) AS rowcount FROM " .
+                               "(SELECT 1 FROM table WHERE field = 'text' AND (column IS NOT NULL)  ) tmp_count"
+                       ],
+                       [
+                               [
+                                       'tables' => 'table',
+                                       'field' => [ 'alias' => 'column' ],
+                                       'conds' => [ 'field' => 'text' ],
+                               ],
+                               "SELECT COUNT(*) AS rowcount FROM " .
+                               "(SELECT 1 FROM table WHERE field = 'text' AND (column IS NOT NULL)  ) tmp_count"
+                       ],
+                       [
+                               [
+                                       'tables' => 'table',
+                                       'field' => [ 'alias' => 'column' ],
+                                       'conds' => '',
+                               ],
+                               "SELECT COUNT(*) AS rowcount FROM " .
+                               "(SELECT 1 FROM table WHERE (column IS NOT NULL)  ) tmp_count"
+                       ],
+                       [
+                               [
+                                       'tables' => 'table',
+                                       'field' => [ 'alias' => 'column' ],
+                                       'conds' => false,
+                               ],
+                               "SELECT COUNT(*) AS rowcount FROM " .
+                               "(SELECT 1 FROM table WHERE (column IS NOT NULL)  ) tmp_count"
+                       ],
+                       [
+                               [
+                                       'tables' => 'table',
+                                       'field' => [ 'alias' => 'column' ],
+                                       'conds' => null,
+                               ],
+                               "SELECT COUNT(*) AS rowcount FROM " .
+                               "(SELECT 1 FROM table WHERE (column IS NOT NULL)  ) tmp_count"
+                       ],
+                       [
+                               [
+                                       'tables' => 'table',
+                                       'field' => [ 'alias' => 'column' ],
+                                       'conds' => '1',
+                               ],
+                               "SELECT COUNT(*) AS rowcount FROM " .
+                               "(SELECT 1 FROM table WHERE (1) AND (column IS NOT NULL)  ) tmp_count"
+                       ],
+                       [
+                               [
+                                       'tables' => 'table',
+                                       'field' => [ 'alias' => 'column' ],
+                                       'conds' => '0',
+                               ],
+                               "SELECT COUNT(*) AS rowcount FROM " .
+                               "(SELECT 1 FROM table WHERE (0) AND (column IS NOT NULL)  ) tmp_count"
+                       ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideUpdate
+        * @covers Wikimedia\Rdbms\Database::update
+        * @covers Wikimedia\Rdbms\Database::makeUpdateOptions
+        * @covers Wikimedia\Rdbms\Database::makeUpdateOptionsArray
+        */
+       public function testUpdate( $sql, $sqlText ) {
+               $this->database->update(
+                       $sql['table'],
+                       $sql['values'],
+                       $sql['conds'],
+                       __METHOD__,
+                       $sql['options'] ?? []
+               );
+               $this->assertLastSql( $sqlText );
+       }
+
+       public static function provideUpdate() {
+               return [
+                       [
+                               [
+                                       'table' => 'table',
+                                       'values' => [ 'field' => 'text', 'field2' => 'text2' ],
+                                       'conds' => [ 'alias' => 'text' ],
+                               ],
+                               "UPDATE table " .
+                                       "SET field = 'text'" .
+                                       ",field2 = 'text2' " .
+                                       "WHERE alias = 'text'"
+                       ],
+                       [
+                               [
+                                       'table' => 'table',
+                                       'values' => [ 'field = other', 'field2' => 'text2' ],
+                                       'conds' => [ 'id' => '1' ],
+                               ],
+                               "UPDATE table " .
+                                       "SET field = other" .
+                                       ",field2 = 'text2' " .
+                                       "WHERE id = '1'"
+                       ],
+                       [
+                               [
+                                       'table' => 'table',
+                                       'values' => [ 'field = other', 'field2' => 'text2' ],
+                                       'conds' => '*',
+                               ],
+                               "UPDATE table " .
+                                       "SET field = other" .
+                                       ",field2 = 'text2'"
+                       ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideDelete
+        * @covers Wikimedia\Rdbms\Database::delete
+        */
+       public function testDelete( $sql, $sqlText ) {
+               $this->database->delete(
+                       $sql['table'],
+                       $sql['conds'],
+                       __METHOD__
+               );
+               $this->assertLastSql( $sqlText );
+       }
+
+       public static function provideDelete() {
+               return [
+                       [
+                               [
+                                       'table' => 'table',
+                                       'conds' => [ 'alias' => 'text' ],
+                               ],
+                               "DELETE FROM table " .
+                                       "WHERE alias = 'text'"
+                       ],
+                       [
+                               [
+                                       'table' => 'table',
+                                       'conds' => '*',
+                               ],
+                               "DELETE FROM table"
+                       ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideUpsert
+        * @covers Wikimedia\Rdbms\Database::upsert
+        */
+       public function testUpsert( $sql, $sqlText ) {
+               $this->database->upsert(
+                       $sql['table'],
+                       $sql['rows'],
+                       $sql['uniqueIndexes'],
+                       $sql['set'],
+                       __METHOD__
+               );
+               $this->assertLastSql( $sqlText );
+       }
+
+       public static function provideUpsert() {
+               return [
+                       [
+                               [
+                                       'table' => 'upsert_table',
+                                       'rows' => [ 'field' => 'text', 'field2' => 'text2' ],
+                                       'uniqueIndexes' => [ 'field' ],
+                                       'set' => [ 'field' => 'set' ],
+                               ],
+                               "BEGIN; " .
+                                       "UPDATE upsert_table " .
+                                       "SET field = 'set' " .
+                                       "WHERE ((field = 'text')); " .
+                                       "INSERT IGNORE INTO upsert_table " .
+                                       "(field,field2) " .
+                                       "VALUES ('text','text2'); " .
+                                       "COMMIT"
+                       ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideDeleteJoin
+        * @covers Wikimedia\Rdbms\Database::deleteJoin
+        */
+       public function testDeleteJoin( $sql, $sqlText ) {
+               $this->database->deleteJoin(
+                       $sql['delTable'],
+                       $sql['joinTable'],
+                       $sql['delVar'],
+                       $sql['joinVar'],
+                       $sql['conds'],
+                       __METHOD__
+               );
+               $this->assertLastSql( $sqlText );
+       }
+
+       public static function provideDeleteJoin() {
+               return [
+                       [
+                               [
+                                       'delTable' => 'table',
+                                       'joinTable' => 'table_join',
+                                       'delVar' => 'field',
+                                       'joinVar' => 'field_join',
+                                       'conds' => [ 'alias' => 'text' ],
+                               ],
+                               "DELETE FROM table " .
+                                       "WHERE field IN (" .
+                                       "SELECT field_join FROM table_join WHERE alias = 'text'" .
+                                       ")"
+                       ],
+                       [
+                               [
+                                       'delTable' => 'table',
+                                       'joinTable' => 'table_join',
+                                       'delVar' => 'field',
+                                       'joinVar' => 'field_join',
+                                       'conds' => '*',
+                               ],
+                               "DELETE FROM table " .
+                                       "WHERE field IN (" .
+                                       "SELECT field_join FROM table_join " .
+                                       ")"
+                       ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideInsert
+        * @covers Wikimedia\Rdbms\Database::insert
+        * @covers Wikimedia\Rdbms\Database::makeInsertOptions
+        */
+       public function testInsert( $sql, $sqlText ) {
+               $this->database->insert(
+                       $sql['table'],
+                       $sql['rows'],
+                       __METHOD__,
+                       $sql['options'] ?? []
+               );
+               $this->assertLastSql( $sqlText );
+       }
+
+       public static function provideInsert() {
+               return [
+                       [
+                               [
+                                       'table' => 'table',
+                                       'rows' => [ 'field' => 'text', 'field2' => 2 ],
+                               ],
+                               "INSERT INTO table " .
+                                       "(field,field2) " .
+                                       "VALUES ('text','2')"
+                       ],
+                       [
+                               [
+                                       'table' => 'table',
+                                       'rows' => [ 'field' => 'text', 'field2' => 2 ],
+                                       'options' => 'IGNORE',
+                               ],
+                               "INSERT IGNORE INTO table " .
+                                       "(field,field2) " .
+                                       "VALUES ('text','2')"
+                       ],
+                       [
+                               [
+                                       'table' => 'table',
+                                       'rows' => [
+                                               [ 'field' => 'text', 'field2' => 2 ],
+                                               [ 'field' => 'multi', 'field2' => 3 ],
+                                       ],
+                                       'options' => 'IGNORE',
+                               ],
+                               "INSERT IGNORE INTO table " .
+                                       "(field,field2) " .
+                                       "VALUES " .
+                                       "('text','2')," .
+                                       "('multi','3')"
+                       ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideInsertSelect
+        * @covers Wikimedia\Rdbms\Database::insertSelect
+        * @covers Wikimedia\Rdbms\Database::nativeInsertSelect
+        */
+       public function testInsertSelect( $sql, $sqlTextNative, $sqlSelect, $sqlInsert ) {
+               $this->database->insertSelect(
+                       $sql['destTable'],
+                       $sql['srcTable'],
+                       $sql['varMap'],
+                       $sql['conds'],
+                       __METHOD__,
+                       $sql['insertOptions'] ?? [],
+                       $sql['selectOptions'] ?? [],
+                       $sql['selectJoinConds'] ?? []
+               );
+               $this->assertLastSql( $sqlTextNative );
+
+               $dbWeb = new DatabaseTestHelper( __CLASS__, [ 'cliMode' => false ] );
+               $dbWeb->forceNextResult( [
+                       array_flip( array_keys( $sql['varMap'] ) )
+               ] );
+               $dbWeb->insertSelect(
+                       $sql['destTable'],
+                       $sql['srcTable'],
+                       $sql['varMap'],
+                       $sql['conds'],
+                       __METHOD__,
+                       $sql['insertOptions'] ?? [],
+                       $sql['selectOptions'] ?? [],
+                       $sql['selectJoinConds'] ?? []
+               );
+               $this->assertLastSqlDb( implode( '; ', [ $sqlSelect, 'BEGIN', $sqlInsert, 'COMMIT' ] ), $dbWeb );
+       }
+
+       public static function provideInsertSelect() {
+               return [
+                       [
+                               [
+                                       'destTable' => 'insert_table',
+                                       'srcTable' => 'select_table',
+                                       'varMap' => [ 'field_insert' => 'field_select', 'field' => 'field2' ],
+                                       'conds' => '*',
+                               ],
+                               "INSERT INTO insert_table " .
+                                       "(field_insert,field) " .
+                                       "SELECT field_select,field2 " .
+                                       "FROM select_table",
+                               "SELECT field_select AS field_insert,field2 AS field " .
+                               "FROM select_table      FOR UPDATE",
+                               "INSERT INTO insert_table (field_insert,field) VALUES ('0','1')"
+                       ],
+                       [
+                               [
+                                       'destTable' => 'insert_table',
+                                       'srcTable' => 'select_table',
+                                       'varMap' => [ 'field_insert' => 'field_select', 'field' => 'field2' ],
+                                       'conds' => [ 'field' => 2 ],
+                               ],
+                               "INSERT INTO insert_table " .
+                                       "(field_insert,field) " .
+                                       "SELECT field_select,field2 " .
+                                       "FROM select_table " .
+                                       "WHERE field = '2'",
+                               "SELECT field_select AS field_insert,field2 AS field FROM " .
+                               "select_table WHERE field = '2'   FOR UPDATE",
+                               "INSERT INTO insert_table (field_insert,field) VALUES ('0','1')"
+                       ],
+                       [
+                               [
+                                       'destTable' => 'insert_table',
+                                       'srcTable' => 'select_table',
+                                       'varMap' => [ 'field_insert' => 'field_select', 'field' => 'field2' ],
+                                       'conds' => [ 'field' => 2 ],
+                                       'insertOptions' => 'IGNORE',
+                                       'selectOptions' => [ 'ORDER BY' => 'field' ],
+                               ],
+                               "INSERT IGNORE INTO insert_table " .
+                                       "(field_insert,field) " .
+                                       "SELECT field_select,field2 " .
+                                       "FROM select_table " .
+                                       "WHERE field = '2' " .
+                                       "ORDER BY field",
+                               "SELECT field_select AS field_insert,field2 AS field " .
+                               "FROM select_table WHERE field = '2' ORDER BY field  FOR UPDATE",
+                               "INSERT IGNORE INTO insert_table (field_insert,field) VALUES ('0','1')"
+                       ],
+                       [
+                               [
+                                       'destTable' => 'insert_table',
+                                       'srcTable' => [ 'select_table1', 'select_table2' ],
+                                       'varMap' => [ 'field_insert' => 'field_select', 'field' => 'field2' ],
+                                       'conds' => [ 'field' => 2 ],
+                                       'insertOptions' => [ 'NO_AUTO_COLUMNS' ],
+                                       'selectOptions' => [ 'ORDER BY' => 'field', 'FORCE INDEX' => [ 'select_table1' => 'index1' ] ],
+                                       'selectJoinConds' => [
+                                               'select_table2' => [ 'LEFT JOIN', [ 'select_table1.foo = select_table2.bar' ] ],
+                                       ],
+                               ],
+                               "INSERT INTO insert_table " .
+                                       "(field_insert,field) " .
+                                       "SELECT field_select,field2 " .
+                                       "FROM select_table1 LEFT JOIN select_table2 ON ((select_table1.foo = select_table2.bar)) " .
+                                       "WHERE field = '2' " .
+                                       "ORDER BY field",
+                               "SELECT field_select AS field_insert,field2 AS field " .
+                               "FROM select_table1 LEFT JOIN select_table2 ON ((select_table1.foo = select_table2.bar)) " .
+                               "WHERE field = '2' ORDER BY field  FOR UPDATE",
+                               "INSERT INTO insert_table (field_insert,field) VALUES ('0','1')"
+                       ],
+               ];
+       }
+
+       /**
+        * @covers Wikimedia\Rdbms\Database::insertSelect
+        * @covers Wikimedia\Rdbms\Database::nativeInsertSelect
+        */
+       public function testInsertSelectBatching() {
+               $dbWeb = new DatabaseTestHelper( __CLASS__, [ 'cliMode' => false ] );
+               $rows = [];
+               for ( $i = 0; $i <= 25000; $i++ ) {
+                       $rows[] = [ 'field' => $i ];
+               }
+               $dbWeb->forceNextResult( $rows );
+               $dbWeb->insertSelect(
+                       'insert_table',
+                       'select_table',
+                       [ 'field' => 'field2' ],
+                       '*',
+                       __METHOD__
+               );
+               $this->assertLastSqlDb( implode( '; ', [
+                       'SELECT field2 AS field FROM select_table      FOR UPDATE',
+                       'BEGIN',
+                       "INSERT INTO insert_table (field) VALUES ('" . implode( "'),('", range( 0, 9999 ) ) . "')",
+                       "INSERT INTO insert_table (field) VALUES ('" . implode( "'),('", range( 10000, 19999 ) ) . "')",
+                       "INSERT INTO insert_table (field) VALUES ('" . implode( "'),('", range( 20000, 25000 ) ) . "')",
+                       'COMMIT'
+               ] ), $dbWeb );
+       }
+
+       /**
+        * @dataProvider provideReplace
+        * @covers Wikimedia\Rdbms\Database::replace
+        */
+       public function testReplace( $sql, $sqlText ) {
+               $this->database->replace(
+                       $sql['table'],
+                       $sql['uniqueIndexes'],
+                       $sql['rows'],
+                       __METHOD__
+               );
+               $this->assertLastSql( $sqlText );
+       }
+
+       public static function provideReplace() {
+               return [
+                       [
+                               [
+                                       'table' => 'replace_table',
+                                       'uniqueIndexes' => [ 'field' ],
+                                       'rows' => [ 'field' => 'text', 'field2' => 'text2' ],
+                               ],
+                               "BEGIN; DELETE FROM replace_table " .
+                                       "WHERE (field = 'text'); " .
+                                       "INSERT INTO replace_table " .
+                                       "(field,field2) " .
+                                       "VALUES ('text','text2'); COMMIT"
+                       ],
+                       [
+                               [
+                                       'table' => 'module_deps',
+                                       'uniqueIndexes' => [ [ 'md_module', 'md_skin' ] ],
+                                       'rows' => [
+                                               'md_module' => 'module',
+                                               'md_skin' => 'skin',
+                                               'md_deps' => 'deps',
+                                       ],
+                               ],
+                               "BEGIN; DELETE FROM module_deps " .
+                                       "WHERE (md_module = 'module' AND md_skin = 'skin'); " .
+                                       "INSERT INTO module_deps " .
+                                       "(md_module,md_skin,md_deps) " .
+                                       "VALUES ('module','skin','deps'); COMMIT"
+                       ],
+                       [
+                               [
+                                       'table' => 'module_deps',
+                                       'uniqueIndexes' => [ [ 'md_module', 'md_skin' ] ],
+                                       'rows' => [
+                                               [
+                                                       'md_module' => 'module',
+                                                       'md_skin' => 'skin',
+                                                       'md_deps' => 'deps',
+                                               ], [
+                                                       'md_module' => 'module2',
+                                                       'md_skin' => 'skin2',
+                                                       'md_deps' => 'deps2',
+                                               ],
+                                       ],
+                               ],
+                               "BEGIN; DELETE FROM module_deps " .
+                                       "WHERE (md_module = 'module' AND md_skin = 'skin'); " .
+                                       "INSERT INTO module_deps " .
+                                       "(md_module,md_skin,md_deps) " .
+                                       "VALUES ('module','skin','deps'); " .
+                                       "DELETE FROM module_deps " .
+                                       "WHERE (md_module = 'module2' AND md_skin = 'skin2'); " .
+                                       "INSERT INTO module_deps " .
+                                       "(md_module,md_skin,md_deps) " .
+                                       "VALUES ('module2','skin2','deps2'); COMMIT"
+                       ],
+                       [
+                               [
+                                       'table' => 'module_deps',
+                                       'uniqueIndexes' => [ 'md_module', 'md_skin' ],
+                                       'rows' => [
+                                               [
+                                                       'md_module' => 'module',
+                                                       'md_skin' => 'skin',
+                                                       'md_deps' => 'deps',
+                                               ], [
+                                                       'md_module' => 'module2',
+                                                       'md_skin' => 'skin2',
+                                                       'md_deps' => 'deps2',
+                                               ],
+                                       ],
+                               ],
+                               "BEGIN; DELETE FROM module_deps " .
+                                       "WHERE (md_module = 'module') OR (md_skin = 'skin'); " .
+                                       "INSERT INTO module_deps " .
+                                       "(md_module,md_skin,md_deps) " .
+                                       "VALUES ('module','skin','deps'); " .
+                                       "DELETE FROM module_deps " .
+                                       "WHERE (md_module = 'module2') OR (md_skin = 'skin2'); " .
+                                       "INSERT INTO module_deps " .
+                                       "(md_module,md_skin,md_deps) " .
+                                       "VALUES ('module2','skin2','deps2'); COMMIT"
+                       ],
+                       [
+                               [
+                                       'table' => 'module_deps',
+                                       'uniqueIndexes' => [],
+                                       'rows' => [
+                                               'md_module' => 'module',
+                                               'md_skin' => 'skin',
+                                               'md_deps' => 'deps',
+                                       ],
+                               ],
+                               "BEGIN; INSERT INTO module_deps " .
+                                       "(md_module,md_skin,md_deps) " .
+                                       "VALUES ('module','skin','deps'); COMMIT"
+                       ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideNativeReplace
+        * @covers Wikimedia\Rdbms\Database::nativeReplace
+        */
+       public function testNativeReplace( $sql, $sqlText ) {
+               $this->database->nativeReplace(
+                       $sql['table'],
+                       $sql['rows'],
+                       __METHOD__
+               );
+               $this->assertLastSql( $sqlText );
+       }
+
+       public static function provideNativeReplace() {
+               return [
+                       [
+                               [
+                                       'table' => 'replace_table',
+                                       'rows' => [ 'field' => 'text', 'field2' => 'text2' ],
+                               ],
+                               "REPLACE INTO replace_table " .
+                                       "(field,field2) " .
+                                       "VALUES ('text','text2')"
+                       ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideConditional
+        * @covers Wikimedia\Rdbms\Database::conditional
+        */
+       public function testConditional( $sql, $sqlText ) {
+               $this->assertEquals( trim( $this->database->conditional(
+                       $sql['conds'],
+                       $sql['true'],
+                       $sql['false']
+               ) ), $sqlText );
+       }
+
+       public static function provideConditional() {
+               return [
+                       [
+                               [
+                                       'conds' => [ 'field' => 'text' ],
+                                       'true' => 1,
+                                       'false' => 'NULL',
+                               ],
+                               "(CASE WHEN field = 'text' THEN 1 ELSE NULL END)"
+                       ],
+                       [
+                               [
+                                       'conds' => [ 'field' => 'text', 'field2' => 'anothertext' ],
+                                       'true' => 1,
+                                       'false' => 'NULL',
+                               ],
+                               "(CASE WHEN field = 'text' AND field2 = 'anothertext' THEN 1 ELSE NULL END)"
+                       ],
+                       [
+                               [
+                                       'conds' => 'field=1',
+                                       'true' => 1,
+                                       'false' => 'NULL',
+                               ],
+                               "(CASE WHEN field=1 THEN 1 ELSE NULL END)"
+                       ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideBuildConcat
+        * @covers Wikimedia\Rdbms\Database::buildConcat
+        */
+       public function testBuildConcat( $stringList, $sqlText ) {
+               $this->assertEquals( trim( $this->database->buildConcat(
+                       $stringList
+               ) ), $sqlText );
+       }
+
+       public static function provideBuildConcat() {
+               return [
+                       [
+                               [ 'field', 'field2' ],
+                               "CONCAT(field,field2)"
+                       ],
+                       [
+                               [ "'test'", 'field2' ],
+                               "CONCAT('test',field2)"
+                       ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideBuildLike
+        * @covers Wikimedia\Rdbms\Database::buildLike
+        * @covers Wikimedia\Rdbms\Database::escapeLikeInternal
+        */
+       public function testBuildLike( $array, $sqlText ) {
+               $this->assertEquals( trim( $this->database->buildLike(
+                       $array
+               ) ), $sqlText );
+       }
+
+       public static function provideBuildLike() {
+               return [
+                       [
+                               'text',
+                               "LIKE 'text' ESCAPE '`'"
+                       ],
+                       [
+                               [ 'text', new LikeMatch( '%' ) ],
+                               "LIKE 'text%' ESCAPE '`'"
+                       ],
+                       [
+                               [ 'text', new LikeMatch( '%' ), 'text2' ],
+                               "LIKE 'text%text2' ESCAPE '`'"
+                       ],
+                       [
+                               [ 'text', new LikeMatch( '_' ) ],
+                               "LIKE 'text_' ESCAPE '`'"
+                       ],
+                       [
+                               'more_text',
+                               "LIKE 'more`_text' ESCAPE '`'"
+                       ],
+                       [
+                               [ 'C:\\Windows\\', new LikeMatch( '%' ) ],
+                               "LIKE 'C:\\Windows\\%' ESCAPE '`'"
+                       ],
+                       [
+                               [ 'accent`_test`', new LikeMatch( '%' ) ],
+                               "LIKE 'accent```_test``%' ESCAPE '`'"
+                       ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideUnionQueries
+        * @covers Wikimedia\Rdbms\Database::unionQueries
+        */
+       public function testUnionQueries( $sql, $sqlText ) {
+               $this->assertEquals( trim( $this->database->unionQueries(
+                       $sql['sqls'],
+                       $sql['all']
+               ) ), $sqlText );
+       }
+
+       public static function provideUnionQueries() {
+               return [
+                       [
+                               [
+                                       'sqls' => [ 'RAW SQL', 'RAW2SQL' ],
+                                       'all' => true,
+                               ],
+                               "(RAW SQL) UNION ALL (RAW2SQL)"
+                       ],
+                       [
+                               [
+                                       'sqls' => [ 'RAW SQL', 'RAW2SQL' ],
+                                       'all' => false,
+                               ],
+                               "(RAW SQL) UNION (RAW2SQL)"
+                       ],
+                       [
+                               [
+                                       'sqls' => [ 'RAW SQL', 'RAW2SQL', 'RAW3SQL' ],
+                                       'all' => false,
+                               ],
+                               "(RAW SQL) UNION (RAW2SQL) UNION (RAW3SQL)"
+                       ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideUnionConditionPermutations
+        * @covers Wikimedia\Rdbms\Database::unionConditionPermutations
+        */
+       public function testUnionConditionPermutations( $params, $expect ) {
+               if ( isset( $params['unionSupportsOrderAndLimit'] ) ) {
+                       $this->database->setUnionSupportsOrderAndLimit( $params['unionSupportsOrderAndLimit'] );
+               }
+
+               $sql = trim( $this->database->unionConditionPermutations(
+                       $params['table'],
+                       $params['vars'],
+                       $params['permute_conds'],
+                       $params['extra_conds'] ?? '',
+                       'FNAME',
+                       $params['options'] ?? [],
+                       $params['join_conds'] ?? []
+               ) );
+               $this->assertEquals( $expect, $sql );
+       }
+
+       public static function provideUnionConditionPermutations() {
+               // phpcs:disable Generic.Files.LineLength
+               return [
+                       [
+                               [
+                                       'table' => [ 'table1', 'table2' ],
+                                       'vars' => [ 'field1', 'alias' => 'field2' ],
+                                       'permute_conds' => [
+                                               'field3' => [ 1, 2, 3 ],
+                                               'duplicates' => [ 4, 5, 4 ],
+                                               'empty' => [],
+                                               'single' => [ 0 ],
+                                       ],
+                                       'extra_conds' => 'table2.bar > 23',
+                                       'options' => [
+                                               'ORDER BY' => [ 'field1', 'alias' ],
+                                               'INNER ORDER BY' => [ 'field1', 'field2' ],
+                                               'LIMIT' => 100,
+                                       ],
+                                       'join_conds' => [
+                                               'table2' => [ 'JOIN', 'table1.foo_id = table2.foo_id' ],
+                                       ],
+                               ],
+                               "(SELECT  field1,field2 AS alias  FROM table1 JOIN table2 ON ((table1.foo_id = table2.foo_id))   WHERE field3 = '1' AND duplicates = '4' AND single = '0' AND (table2.bar > 23)  ORDER BY field1,field2 LIMIT 100  ) UNION ALL " .
+                               "(SELECT  field1,field2 AS alias  FROM table1 JOIN table2 ON ((table1.foo_id = table2.foo_id))   WHERE field3 = '1' AND duplicates = '5' AND single = '0' AND (table2.bar > 23)  ORDER BY field1,field2 LIMIT 100  ) UNION ALL " .
+                               "(SELECT  field1,field2 AS alias  FROM table1 JOIN table2 ON ((table1.foo_id = table2.foo_id))   WHERE field3 = '2' AND duplicates = '4' AND single = '0' AND (table2.bar > 23)  ORDER BY field1,field2 LIMIT 100  ) UNION ALL " .
+                               "(SELECT  field1,field2 AS alias  FROM table1 JOIN table2 ON ((table1.foo_id = table2.foo_id))   WHERE field3 = '2' AND duplicates = '5' AND single = '0' AND (table2.bar > 23)  ORDER BY field1,field2 LIMIT 100  ) UNION ALL " .
+                               "(SELECT  field1,field2 AS alias  FROM table1 JOIN table2 ON ((table1.foo_id = table2.foo_id))   WHERE field3 = '3' AND duplicates = '4' AND single = '0' AND (table2.bar > 23)  ORDER BY field1,field2 LIMIT 100  ) UNION ALL " .
+                               "(SELECT  field1,field2 AS alias  FROM table1 JOIN table2 ON ((table1.foo_id = table2.foo_id))   WHERE field3 = '3' AND duplicates = '5' AND single = '0' AND (table2.bar > 23)  ORDER BY field1,field2 LIMIT 100  ) " .
+                               "ORDER BY field1,alias LIMIT 100"
+                       ],
+                       [
+                               [
+                                       'table' => 'foo',
+                                       'vars' => [ 'foo_id' ],
+                                       'permute_conds' => [
+                                               'bar' => [ 1, 2, 3 ],
+                                       ],
+                                       'extra_conds' => [ 'baz' => null ],
+                                       'options' => [
+                                               'NOTALL',
+                                               'ORDER BY' => [ 'foo_id' ],
+                                               'LIMIT' => 25,
+                                       ],
+                               ],
+                               "(SELECT  foo_id  FROM foo    WHERE bar = '1' AND baz IS NULL  ORDER BY foo_id LIMIT 25  ) UNION " .
+                               "(SELECT  foo_id  FROM foo    WHERE bar = '2' AND baz IS NULL  ORDER BY foo_id LIMIT 25  ) UNION " .
+                               "(SELECT  foo_id  FROM foo    WHERE bar = '3' AND baz IS NULL  ORDER BY foo_id LIMIT 25  ) " .
+                               "ORDER BY foo_id LIMIT 25"
+                       ],
+                       [
+                               [
+                                       'table' => 'foo',
+                                       'vars' => [ 'foo_id' ],
+                                       'permute_conds' => [
+                                               'bar' => [ 1, 2, 3 ],
+                                       ],
+                                       'extra_conds' => [ 'baz' => null ],
+                                       'options' => [
+                                               'NOTALL' => true,
+                                               'ORDER BY' => [ 'foo_id' ],
+                                               'LIMIT' => 25,
+                                       ],
+                                       'unionSupportsOrderAndLimit' => false,
+                               ],
+                               "(SELECT  foo_id  FROM foo    WHERE bar = '1' AND baz IS NULL  ) UNION " .
+                               "(SELECT  foo_id  FROM foo    WHERE bar = '2' AND baz IS NULL  ) UNION " .
+                               "(SELECT  foo_id  FROM foo    WHERE bar = '3' AND baz IS NULL  ) " .
+                               "ORDER BY foo_id LIMIT 25"
+                       ],
+                       [
+                               [
+                                       'table' => 'foo',
+                                       'vars' => [ 'foo_id' ],
+                                       'permute_conds' => [],
+                                       'extra_conds' => [ 'baz' => null ],
+                                       'options' => [
+                                               'ORDER BY' => [ 'foo_id' ],
+                                               'LIMIT' => 25,
+                                       ],
+                               ],
+                               "SELECT  foo_id  FROM foo    WHERE baz IS NULL  ORDER BY foo_id LIMIT 25"
+                       ],
+                       [
+                               [
+                                       'table' => 'foo',
+                                       'vars' => [ 'foo_id' ],
+                                       'permute_conds' => [
+                                               'bar' => [],
+                                       ],
+                                       'extra_conds' => [ 'baz' => null ],
+                                       'options' => [
+                                               'ORDER BY' => [ 'foo_id' ],
+                                               'LIMIT' => 25,
+                                       ],
+                               ],
+                               "SELECT  foo_id  FROM foo    WHERE baz IS NULL  ORDER BY foo_id LIMIT 25"
+                       ],
+                       [
+                               [
+                                       'table' => 'foo',
+                                       'vars' => [ 'foo_id' ],
+                                       'permute_conds' => [
+                                               'bar' => [ 1 ],
+                                       ],
+                                       'options' => [
+                                               'ORDER BY' => [ 'foo_id' ],
+                                               'LIMIT' => 25,
+                                               'OFFSET' => 150,
+                                       ],
+                               ],
+                               "SELECT  foo_id  FROM foo    WHERE bar = '1'  ORDER BY foo_id LIMIT 150,25"
+                       ],
+                       [
+                               [
+                                       'table' => 'foo',
+                                       'vars' => [ 'foo_id' ],
+                                       'permute_conds' => [],
+                                       'extra_conds' => [ 'baz' => null ],
+                                       'options' => [
+                                               'ORDER BY' => [ 'foo_id' ],
+                                               'LIMIT' => 25,
+                                               'OFFSET' => 150,
+                                               'INNER ORDER BY' => [ 'bar_id' ],
+                                       ],
+                               ],
+                               "(SELECT  foo_id  FROM foo    WHERE baz IS NULL  ORDER BY bar_id LIMIT 175  ) ORDER BY foo_id LIMIT 150,25"
+                       ],
+                       [
+                               [
+                                       'table' => 'foo',
+                                       'vars' => [ 'foo_id' ],
+                                       'permute_conds' => [],
+                                       'extra_conds' => [ 'baz' => null ],
+                                       'options' => [
+                                               'ORDER BY' => [ 'foo_id' ],
+                                               'LIMIT' => 25,
+                                               'OFFSET' => 150,
+                                               'INNER ORDER BY' => [ 'bar_id' ],
+                                       ],
+                                       'unionSupportsOrderAndLimit' => false,
+                               ],
+                               "SELECT  foo_id  FROM foo    WHERE baz IS NULL  ORDER BY foo_id LIMIT 150,25"
+                       ],
+               ];
+               // phpcs:enable
+       }
+
+       /**
+        * @covers Wikimedia\Rdbms\Database::commit
+        * @covers Wikimedia\Rdbms\Database::doCommit
+        */
+       public function testTransactionCommit() {
+               $this->database->begin( __METHOD__ );
+               $this->database->commit( __METHOD__ );
+               $this->assertLastSql( 'BEGIN; COMMIT' );
+       }
+
+       /**
+        * @covers Wikimedia\Rdbms\Database::rollback
+        * @covers Wikimedia\Rdbms\Database::doRollback
+        */
+       public function testTransactionRollback() {
+               $this->database->begin( __METHOD__ );
+               $this->database->rollback( __METHOD__ );
+               $this->assertLastSql( 'BEGIN; ROLLBACK' );
+       }
+
+       /**
+        * @covers Wikimedia\Rdbms\Database::dropTable
+        */
+       public function testDropTable() {
+               $this->database->setExistingTables( [ 'table' ] );
+               $this->database->dropTable( 'table', __METHOD__ );
+               $this->assertLastSql( 'DROP TABLE table CASCADE' );
+       }
+
+       /**
+        * @covers Wikimedia\Rdbms\Database::dropTable
+        */
+       public function testDropNonExistingTable() {
+               $this->assertFalse(
+                       $this->database->dropTable( 'non_existing', __METHOD__ )
+               );
+       }
+
+       /**
+        * @dataProvider provideMakeList
+        * @covers Wikimedia\Rdbms\Database::makeList
+        */
+       public function testMakeList( $list, $mode, $sqlText ) {
+               $this->assertEquals( trim( $this->database->makeList(
+                       $list, $mode
+               ) ), $sqlText );
+       }
+
+       public static function provideMakeList() {
+               return [
+                       [
+                               [ 'value', 'value2' ],
+                               LIST_COMMA,
+                               "'value','value2'"
+                       ],
+                       [
+                               [ 'field', 'field2' ],
+                               LIST_NAMES,
+                               "field,field2"
+                       ],
+                       [
+                               [ 'field' => 'value', 'field2' => 'value2' ],
+                               LIST_AND,
+                               "field = 'value' AND field2 = 'value2'"
+                       ],
+                       [
+                               [ 'field' => null, "field2 != 'value2'" ],
+                               LIST_AND,
+                               "field IS NULL AND (field2 != 'value2')"
+                       ],
+                       [
+                               [ 'field' => [ 'value', null, 'value2' ], 'field2' => 'value2' ],
+                               LIST_AND,
+                               "(field IN ('value','value2')  OR field IS NULL) AND field2 = 'value2'"
+                       ],
+                       [
+                               [ 'field' => [ null ], 'field2' => null ],
+                               LIST_AND,
+                               "field IS NULL AND field2 IS NULL"
+                       ],
+                       [
+                               [ 'field' => 'value', 'field2' => 'value2' ],
+                               LIST_OR,
+                               "field = 'value' OR field2 = 'value2'"
+                       ],
+                       [
+                               [ 'field' => 'value', 'field2' => null ],
+                               LIST_OR,
+                               "field = 'value' OR field2 IS NULL"
+                       ],
+                       [
+                               [ 'field' => [ 'value', 'value2' ], 'field2' => [ 'value' ] ],
+                               LIST_OR,
+                               "field IN ('value','value2')  OR field2 = 'value'"
+                       ],
+                       [
+                               [ 'field' => [ null, 'value', null, 'value2' ], "field2 != 'value2'" ],
+                               LIST_OR,
+                               "(field IN ('value','value2')  OR field IS NULL) OR (field2 != 'value2')"
+                       ],
+                       [
+                               [ 'field' => 'value', 'field2' => 'value2' ],
+                               LIST_SET,
+                               "field = 'value',field2 = 'value2'"
+                       ],
+                       [
+                               [ 'field' => 'value', 'field2' => null ],
+                               LIST_SET,
+                               "field = 'value',field2 = NULL"
+                       ],
+                       [
+                               [ 'field' => 'value', "field2 != 'value2'" ],
+                               LIST_SET,
+                               "field = 'value',field2 != 'value2'"
+                       ],
+               ];
+       }
+
+       /**
+        * @covers Wikimedia\Rdbms\Database::registerTempTableWrite
+        */
+       public function testSessionTempTables() {
+               $temp1 = $this->database->tableName( 'tmp_table_1' );
+               $temp2 = $this->database->tableName( 'tmp_table_2' );
+               $temp3 = $this->database->tableName( 'tmp_table_3' );
+
+               $this->database->query( "CREATE TEMPORARY TABLE $temp1 LIKE orig_tbl", __METHOD__ );
+               $this->database->query( "CREATE TEMPORARY TABLE $temp2 LIKE orig_tbl", __METHOD__ );
+               $this->database->query( "CREATE TEMPORARY TABLE $temp3 LIKE orig_tbl", __METHOD__ );
+
+               $this->assertTrue( $this->database->tableExists( "tmp_table_1", __METHOD__ ) );
+               $this->assertTrue( $this->database->tableExists( "tmp_table_2", __METHOD__ ) );
+               $this->assertTrue( $this->database->tableExists( "tmp_table_3", __METHOD__ ) );
+
+               $this->database->dropTable( 'tmp_table_1', __METHOD__ );
+               $this->database->dropTable( 'tmp_table_2', __METHOD__ );
+               $this->database->dropTable( 'tmp_table_3', __METHOD__ );
+
+               $this->assertFalse( $this->database->tableExists( "tmp_table_1", __METHOD__ ) );
+               $this->assertFalse( $this->database->tableExists( "tmp_table_2", __METHOD__ ) );
+               $this->assertFalse( $this->database->tableExists( "tmp_table_3", __METHOD__ ) );
+
+               $this->database->query( "CREATE TEMPORARY TABLE tmp_table_1 LIKE orig_tbl", __METHOD__ );
+               $this->database->query( "CREATE TEMPORARY TABLE 'tmp_table_2' LIKE orig_tbl", __METHOD__ );
+               $this->database->query( "CREATE TEMPORARY TABLE `tmp_table_3` LIKE orig_tbl", __METHOD__ );
+
+               $this->assertTrue( $this->database->tableExists( "tmp_table_1", __METHOD__ ) );
+               $this->assertTrue( $this->database->tableExists( "tmp_table_2", __METHOD__ ) );
+               $this->assertTrue( $this->database->tableExists( "tmp_table_3", __METHOD__ ) );
+
+               $this->database->query( "DROP TEMPORARY TABLE tmp_table_1 LIKE orig_tbl", __METHOD__ );
+               $this->database->query( "DROP TEMPORARY TABLE 'tmp_table_2' LIKE orig_tbl", __METHOD__ );
+               $this->database->query( "DROP TABLE `tmp_table_3` LIKE orig_tbl", __METHOD__ );
+
+               $this->assertFalse( $this->database->tableExists( "tmp_table_1", __METHOD__ ) );
+               $this->assertFalse( $this->database->tableExists( "tmp_table_2", __METHOD__ ) );
+               $this->assertFalse( $this->database->tableExists( "tmp_table_3", __METHOD__ ) );
+       }
+
+       public function provideBuildSubstring() {
+               yield [ 'someField', 1, 2, 'SUBSTRING(someField FROM 1 FOR 2)' ];
+               yield [ 'someField', 1, null, 'SUBSTRING(someField FROM 1)' ];
+       }
+
+       /**
+        * @covers Wikimedia\Rdbms\Database::buildSubstring
+        * @dataProvider provideBuildSubstring
+        */
+       public function testBuildSubstring( $input, $start, $length, $expected ) {
+               $output = $this->database->buildSubstring( $input, $start, $length );
+               $this->assertSame( $expected, $output );
+       }
+
+       public function provideBuildSubstring_invalidParams() {
+               yield [ -1, 1 ];
+               yield [ 1, -1 ];
+               yield [ 1, 'foo' ];
+               yield [ 'foo', 1 ];
+               yield [ null, 1 ];
+               yield [ 0, 1 ];
+       }
+
+       /**
+        * @covers Wikimedia\Rdbms\Database::buildSubstring
+        * @covers Wikimedia\Rdbms\Database::assertBuildSubstringParams
+        * @dataProvider provideBuildSubstring_invalidParams
+        */
+       public function testBuildSubstring_invalidParams( $start, $length ) {
+               $this->setExpectedException( InvalidArgumentException::class );
+               $this->database->buildSubstring( 'foo', $start, $length );
+       }
+
+       /**
+        * @covers \Wikimedia\Rdbms\Database::buildIntegerCast
+        */
+       public function testBuildIntegerCast() {
+               $output = $this->database->buildIntegerCast( 'fieldName' );
+               $this->assertSame( 'CAST( fieldName AS INTEGER )', $output );
+       }
+
+       /**
+        * @covers \Wikimedia\Rdbms\Database::doSavepoint
+        * @covers \Wikimedia\Rdbms\Database::doReleaseSavepoint
+        * @covers \Wikimedia\Rdbms\Database::doRollbackToSavepoint
+        * @covers \Wikimedia\Rdbms\Database::startAtomic
+        * @covers \Wikimedia\Rdbms\Database::endAtomic
+        * @covers \Wikimedia\Rdbms\Database::cancelAtomic
+        * @covers \Wikimedia\Rdbms\Database::doAtomicSection
+        */
+       public function testAtomicSections() {
+               $this->database->startAtomic( __METHOD__ );
+               $this->database->endAtomic( __METHOD__ );
+               $this->assertLastSql( 'BEGIN; COMMIT' );
+
+               $this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
+               $this->database->cancelAtomic( __METHOD__ );
+               $this->assertLastSql( 'BEGIN; ROLLBACK' );
+
+               $this->database->begin( __METHOD__ );
+               $this->database->startAtomic( __METHOD__ );
+               $this->database->endAtomic( __METHOD__ );
+               $this->database->commit( __METHOD__ );
+               $this->assertLastSql( 'BEGIN; COMMIT' );
+
+               $this->database->begin( __METHOD__ );
+               $this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
+               $this->database->endAtomic( __METHOD__ );
+               $this->database->commit( __METHOD__ );
+               // phpcs:ignore Generic.Files.LineLength
+               $this->assertLastSql( 'BEGIN; SAVEPOINT wikimedia_rdbms_atomic1; RELEASE SAVEPOINT wikimedia_rdbms_atomic1; COMMIT' );
+
+               $this->database->begin( __METHOD__ );
+               $this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
+               $this->database->cancelAtomic( __METHOD__ );
+               $this->database->commit( __METHOD__ );
+               // phpcs:ignore Generic.Files.LineLength
+               $this->assertLastSql( 'BEGIN; SAVEPOINT wikimedia_rdbms_atomic1; ROLLBACK TO SAVEPOINT wikimedia_rdbms_atomic1; COMMIT' );
+
+               $this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
+               $this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
+               $this->database->cancelAtomic( __METHOD__ );
+               $this->database->endAtomic( __METHOD__ );
+               // phpcs:ignore Generic.Files.LineLength
+               $this->assertLastSql( 'BEGIN; SAVEPOINT wikimedia_rdbms_atomic1; ROLLBACK TO SAVEPOINT wikimedia_rdbms_atomic1; COMMIT' );
+
+               $noOpCallack = function () {
+               };
+
+               $this->database->doAtomicSection( __METHOD__, $noOpCallack, IDatabase::ATOMIC_CANCELABLE );
+               $this->assertLastSql( 'BEGIN; COMMIT' );
+
+               $this->database->doAtomicSection( __METHOD__, $noOpCallack );
+               $this->assertLastSql( 'BEGIN; COMMIT' );
+
+               $this->database->begin( __METHOD__ );
+               $this->database->doAtomicSection( __METHOD__, $noOpCallack, IDatabase::ATOMIC_CANCELABLE );
+               $this->database->rollback( __METHOD__ );
+               // phpcs:ignore Generic.Files.LineLength
+               $this->assertLastSql( 'BEGIN; SAVEPOINT wikimedia_rdbms_atomic1; RELEASE SAVEPOINT wikimedia_rdbms_atomic1; ROLLBACK' );
+
+               $fname = __METHOD__;
+               $triggerMap = [
+                       '-' => '-',
+                       IDatabase::TRIGGER_COMMIT => 'tCommit',
+                       IDatabase::TRIGGER_ROLLBACK => 'tRollback'
+               ];
+               $pcCallback = function ( IDatabase $db ) use ( $fname ) {
+                       $this->database->query( "SELECT 0", $fname );
+               };
+               $callback1 = function ( $trigger = '-' ) use ( $fname, $triggerMap ) {
+                       $this->database->query( "SELECT 1, {$triggerMap[$trigger]} AS t", $fname );
+               };
+               $callback2 = function ( $trigger = '-' ) use ( $fname, $triggerMap ) {
+                       $this->database->query( "SELECT 2, {$triggerMap[$trigger]} AS t", $fname );
+               };
+               $callback3 = function ( $trigger = '-' ) use ( $fname, $triggerMap ) {
+                       $this->database->query( "SELECT 3, {$triggerMap[$trigger]} AS t", $fname );
+               };
+
+               $this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
+               $this->database->onTransactionPreCommitOrIdle( $pcCallback, __METHOD__ );
+               $this->database->cancelAtomic( __METHOD__ );
+               $this->assertLastSql( 'BEGIN; ROLLBACK' );
+
+               $this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
+               $this->database->onTransactionCommitOrIdle( $callback1, __METHOD__ );
+               $this->database->cancelAtomic( __METHOD__ );
+               $this->assertLastSql( 'BEGIN; ROLLBACK' );
+
+               $this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
+               $this->database->onTransactionResolution( $callback1, __METHOD__ );
+               $this->database->cancelAtomic( __METHOD__ );
+               $this->assertLastSql( 'BEGIN; ROLLBACK; SELECT 1, tRollback AS t' );
+
+               $this->database->startAtomic( __METHOD__ . '_outer' );
+               $this->database->onTransactionPreCommitOrIdle( $pcCallback, __METHOD__ );
+               $this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
+               $this->database->onTransactionPreCommitOrIdle( $pcCallback, __METHOD__ );
+               $this->database->cancelAtomic( __METHOD__ );
+               $this->database->onTransactionPreCommitOrIdle( $pcCallback, __METHOD__ );
+               $this->database->endAtomic( __METHOD__ . '_outer' );
+               $this->assertLastSql( implode( "; ", [
+                       'BEGIN',
+                       'SAVEPOINT wikimedia_rdbms_atomic1',
+                       'ROLLBACK TO SAVEPOINT wikimedia_rdbms_atomic1',
+                       'SELECT 0',
+                       'SELECT 0',
+                       'COMMIT'
+               ] ) );
+
+               $this->database->startAtomic( __METHOD__ . '_outer' );
+               $this->database->onTransactionCommitOrIdle( $callback1, __METHOD__ );
+               $this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
+               $this->database->onTransactionCommitOrIdle( $callback2, __METHOD__ );
+               $this->database->cancelAtomic( __METHOD__ );
+               $this->database->onTransactionCommitOrIdle( $callback3, __METHOD__ );
+               $this->database->endAtomic( __METHOD__ . '_outer' );
+               $this->assertLastSql( implode( "; ", [
+                       'BEGIN',
+                       'SAVEPOINT wikimedia_rdbms_atomic1',
+                       'ROLLBACK TO SAVEPOINT wikimedia_rdbms_atomic1',
+                       'COMMIT',
+                       'SELECT 1, tCommit AS t',
+                       'SELECT 3, tCommit AS t'
+               ] ) );
+
+               $this->database->startAtomic( __METHOD__ . '_outer' );
+               $this->database->onTransactionResolution( $callback1, __METHOD__ );
+               $this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
+               $this->database->onTransactionResolution( $callback2, __METHOD__ );
+               $this->database->cancelAtomic( __METHOD__ );
+               $this->database->onTransactionResolution( $callback3, __METHOD__ );
+               $this->database->endAtomic( __METHOD__ . '_outer' );
+               $this->assertLastSql( implode( "; ", [
+                       'BEGIN',
+                       'SAVEPOINT wikimedia_rdbms_atomic1',
+                       'ROLLBACK TO SAVEPOINT wikimedia_rdbms_atomic1',
+                       'COMMIT',
+                       'SELECT 1, tCommit AS t',
+                       'SELECT 2, tRollback AS t',
+                       'SELECT 3, tCommit AS t'
+               ] ) );
+
+               $makeCallback = function ( $id ) use ( $fname, $triggerMap ) {
+                       return function ( $trigger = '-' ) use ( $id, $fname, $triggerMap ) {
+                               $this->database->query( "SELECT $id, {$triggerMap[$trigger]} AS t", $fname );
+                       };
+               };
+
+               $this->database->startAtomic( __METHOD__ . '_outer' );
+               $this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
+               $this->database->onTransactionResolution( $makeCallback( 1 ), __METHOD__ );
+               $this->database->cancelAtomic( __METHOD__ );
+               $this->database->endAtomic( __METHOD__ . '_outer' );
+               $this->assertLastSql( implode( "; ", [
+                       'BEGIN',
+                       'SAVEPOINT wikimedia_rdbms_atomic1',
+                       'ROLLBACK TO SAVEPOINT wikimedia_rdbms_atomic1',
+                       'COMMIT',
+                       'SELECT 1, tRollback AS t'
+               ] ) );
+
+               $this->database->startAtomic( __METHOD__ . '_level1', IDatabase::ATOMIC_CANCELABLE );
+               $this->database->onTransactionResolution( $makeCallback( 1 ), __METHOD__ );
+               $this->database->startAtomic( __METHOD__ . '_level2' );
+               $this->database->startAtomic( __METHOD__ . '_level3', IDatabase::ATOMIC_CANCELABLE );
+               $this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
+               $this->database->onTransactionResolution( $makeCallback( 2 ), __METHOD__ );
+               $this->database->endAtomic( __METHOD__ );
+               $this->database->onTransactionResolution( $makeCallback( 3 ), __METHOD__ );
+               $this->database->cancelAtomic( __METHOD__ . '_level3' );
+               $this->database->endAtomic( __METHOD__ . '_level2' );
+               $this->database->onTransactionResolution( $makeCallback( 4 ), __METHOD__ );
+               $this->database->endAtomic( __METHOD__ . '_level1' );
+               $this->assertLastSql( implode( "; ", [
+                       'BEGIN',
+                       'SAVEPOINT wikimedia_rdbms_atomic1',
+                       'SAVEPOINT wikimedia_rdbms_atomic2',
+                       'RELEASE SAVEPOINT wikimedia_rdbms_atomic2',
+                       'ROLLBACK TO SAVEPOINT wikimedia_rdbms_atomic1',
+                       'COMMIT; SELECT 1, tCommit AS t',
+                       'SELECT 2, tRollback AS t',
+                       'SELECT 3, tRollback AS t',
+                       'SELECT 4, tCommit AS t'
+               ] ) );
+       }
+
+       /**
+        * @covers \Wikimedia\Rdbms\Database::doSavepoint
+        * @covers \Wikimedia\Rdbms\Database::doReleaseSavepoint
+        * @covers \Wikimedia\Rdbms\Database::doRollbackToSavepoint
+        * @covers \Wikimedia\Rdbms\Database::startAtomic
+        * @covers \Wikimedia\Rdbms\Database::endAtomic
+        * @covers \Wikimedia\Rdbms\Database::cancelAtomic
+        * @covers \Wikimedia\Rdbms\Database::doAtomicSection
+        */
+       public function testAtomicSectionsRecovery() {
+               $this->database->begin( __METHOD__ );
+               try {
+                       $this->database->doAtomicSection(
+                               __METHOD__,
+                               function () {
+                                       $this->database->startAtomic( 'inner_func1' );
+                                       $this->database->startAtomic( 'inner_func2' );
+
+                                       throw new RuntimeException( 'Test exception' );
+                               },
+                               IDatabase::ATOMIC_CANCELABLE
+                       );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( RuntimeException $ex ) {
+                       $this->assertSame( 'Test exception', $ex->getMessage() );
+               }
+               $this->database->commit( __METHOD__ );
+               // phpcs:ignore Generic.Files.LineLength
+               $this->assertLastSql( 'BEGIN; SAVEPOINT wikimedia_rdbms_atomic1; ROLLBACK TO SAVEPOINT wikimedia_rdbms_atomic1; COMMIT' );
+
+               $this->database->begin( __METHOD__ );
+               try {
+                       $this->database->doAtomicSection(
+                               __METHOD__,
+                               function () {
+                                       throw new RuntimeException( 'Test exception' );
+                               }
+                       );
+                       $this->fail( 'Test exception not thrown' );
+               } catch ( RuntimeException $ex ) {
+                       $this->assertSame( 'Test exception', $ex->getMessage() );
+               }
+               try {
+                       $this->database->commit( __METHOD__ );
+                       $this->fail( 'Test exception not thrown' );
+               } catch ( DBTransactionError $ex ) {
+                       $this->assertSame(
+                               'Cannot execute query from ' . __METHOD__ . ' while transaction status is ERROR.',
+                               $ex->getMessage()
+                       );
+               }
+               $this->database->rollback( __METHOD__ );
+               $this->assertLastSql( 'BEGIN; ROLLBACK' );
+       }
+
+       /**
+        * @covers \Wikimedia\Rdbms\Database::doSavepoint
+        * @covers \Wikimedia\Rdbms\Database::doReleaseSavepoint
+        * @covers \Wikimedia\Rdbms\Database::doRollbackToSavepoint
+        * @covers \Wikimedia\Rdbms\Database::startAtomic
+        * @covers \Wikimedia\Rdbms\Database::endAtomic
+        * @covers \Wikimedia\Rdbms\Database::cancelAtomic
+        * @covers \Wikimedia\Rdbms\Database::doAtomicSection
+        */
+       public function testAtomicSectionsCallbackCancellation() {
+               $fname = __METHOD__;
+               $callback1Called = null;
+               $callback1 = function ( $trigger = '-' ) use ( $fname, &$callback1Called ) {
+                       $callback1Called = $trigger;
+                       $this->database->query( "SELECT 1", $fname );
+               };
+               $callback2Called = null;
+               $callback2 = function ( $trigger = '-' ) use ( $fname, &$callback2Called ) {
+                       $callback2Called = $trigger;
+                       $this->database->query( "SELECT 2", $fname );
+               };
+               $callback3Called = null;
+               $callback3 = function ( $trigger = '-' ) use ( $fname, &$callback3Called ) {
+                       $callback3Called = $trigger;
+                       $this->database->query( "SELECT 3", $fname );
+               };
+
+               $this->database->startAtomic( __METHOD__ . '_outer' );
+               $this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
+               $this->database->startAtomic( __METHOD__ . '_inner' );
+               $this->database->onTransactionCommitOrIdle( $callback1, __METHOD__ );
+               $this->database->onTransactionPreCommitOrIdle( $callback2, __METHOD__ );
+               $this->database->onTransactionResolution( $callback3, __METHOD__ );
+               $this->database->endAtomic( __METHOD__ . '_inner' );
+               $this->database->cancelAtomic( __METHOD__ );
+               $this->database->endAtomic( __METHOD__ . '_outer' );
+               $this->assertNull( $callback1Called );
+               $this->assertNull( $callback2Called );
+               $this->assertEquals( IDatabase::TRIGGER_ROLLBACK, $callback3Called );
+               // phpcs:ignore Generic.Files.LineLength
+               $this->assertLastSql( 'BEGIN; SAVEPOINT wikimedia_rdbms_atomic1; ROLLBACK TO SAVEPOINT wikimedia_rdbms_atomic1; COMMIT; SELECT 3' );
+
+               $callback1Called = null;
+               $callback2Called = null;
+               $callback3Called = null;
+               $this->database->startAtomic( __METHOD__ . '_outer' );
+               $this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
+               $this->database->startAtomic( __METHOD__ . '_inner', IDatabase::ATOMIC_CANCELABLE );
+               $this->database->onTransactionCommitOrIdle( $callback1, __METHOD__ );
+               $this->database->onTransactionPreCommitOrIdle( $callback2, __METHOD__ );
+               $this->database->onTransactionResolution( $callback3, __METHOD__ );
+               $this->database->endAtomic( __METHOD__ . '_inner' );
+               $this->database->cancelAtomic( __METHOD__ );
+               $this->database->endAtomic( __METHOD__ . '_outer' );
+               $this->assertNull( $callback1Called );
+               $this->assertNull( $callback2Called );
+               $this->assertEquals( IDatabase::TRIGGER_ROLLBACK, $callback3Called );
+               // phpcs:ignore Generic.Files.LineLength
+               $this->assertLastSql( 'BEGIN; SAVEPOINT wikimedia_rdbms_atomic1; SAVEPOINT wikimedia_rdbms_atomic2; RELEASE SAVEPOINT wikimedia_rdbms_atomic2; ROLLBACK TO SAVEPOINT wikimedia_rdbms_atomic1; COMMIT; SELECT 3' );
+
+               $callback1Called = null;
+               $callback2Called = null;
+               $callback3Called = null;
+               $this->database->startAtomic( __METHOD__ . '_outer' );
+               $atomicId = $this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
+               $this->database->startAtomic( __METHOD__ . '_inner' );
+               $this->database->onTransactionCommitOrIdle( $callback1, __METHOD__ );
+               $this->database->onTransactionPreCommitOrIdle( $callback2, __METHOD__ );
+               $this->database->onTransactionResolution( $callback3, __METHOD__ );
+               $this->database->cancelAtomic( __METHOD__, $atomicId );
+               $this->database->endAtomic( __METHOD__ . '_outer' );
+               $this->assertNull( $callback1Called );
+               $this->assertNull( $callback2Called );
+               $this->assertEquals( IDatabase::TRIGGER_ROLLBACK, $callback3Called );
+
+               $callback1Called = null;
+               $callback2Called = null;
+               $callback3Called = null;
+               $this->database->startAtomic( __METHOD__ . '_outer' );
+               $atomicId = $this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
+               $this->database->startAtomic( __METHOD__ . '_inner' );
+               $this->database->onTransactionCommitOrIdle( $callback1, __METHOD__ );
+               $this->database->onTransactionPreCommitOrIdle( $callback2, __METHOD__ );
+               $this->database->onTransactionResolution( $callback3, __METHOD__ );
+               try {
+                       $this->database->cancelAtomic( __METHOD__ . '_X', $atomicId );
+               } catch ( DBUnexpectedError $e ) {
+                       $m = __METHOD__;
+                       $this->assertSame(
+                               "Invalid atomic section ended (got {$m}_X but expected {$m}).",
+                               $e->getMessage()
+                       );
+               }
+               $this->database->cancelAtomic( __METHOD__ );
+               $this->database->endAtomic( __METHOD__ . '_outer' );
+               $this->assertNull( $callback1Called );
+               $this->assertNull( $callback2Called );
+               $this->assertEquals( IDatabase::TRIGGER_ROLLBACK, $callback3Called );
+
+               $this->database->startAtomic( __METHOD__ . '_outer' );
+               $this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
+               $this->database->startAtomic( __METHOD__ . '_inner' );
+               $this->database->onTransactionCommitOrIdle( $callback1, __METHOD__ );
+               $this->database->onTransactionPreCommitOrIdle( $callback2, __METHOD__ );
+               $this->database->onTransactionResolution( $callback3, __METHOD__ );
+               $this->database->cancelAtomic( __METHOD__ . '_inner' );
+               $this->database->cancelAtomic( __METHOD__ );
+               $this->database->endAtomic( __METHOD__ . '_outer' );
+               $this->assertNull( $callback1Called );
+               $this->assertNull( $callback2Called );
+               $this->assertEquals( IDatabase::TRIGGER_ROLLBACK, $callback3Called );
+
+               $wrapper = TestingAccessWrapper::newFromObject( $this->database );
+               $callback1Called = null;
+               $callback2Called = null;
+               $callback3Called = null;
+               $this->database->startAtomic( __METHOD__ . '_outer' );
+               $this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
+               $this->database->startAtomic( __METHOD__ . '_inner' );
+               $this->database->onTransactionCommitOrIdle( $callback1, __METHOD__ );
+               $this->database->onTransactionPreCommitOrIdle( $callback2, __METHOD__ );
+               $this->database->onTransactionResolution( $callback3, __METHOD__ );
+               $wrapper->trxStatus = Database::STATUS_TRX_ERROR;
+               $this->database->cancelAtomic( __METHOD__ . '_inner' );
+               $this->database->cancelAtomic( __METHOD__ );
+               $this->database->endAtomic( __METHOD__ . '_outer' );
+               $this->assertNull( $callback1Called );
+               $this->assertNull( $callback2Called );
+               $this->assertEquals( IDatabase::TRIGGER_ROLLBACK, $callback3Called );
+       }
+
+       /**
+        * @covers \Wikimedia\Rdbms\Database::doSavepoint
+        * @covers \Wikimedia\Rdbms\Database::doReleaseSavepoint
+        * @covers \Wikimedia\Rdbms\Database::doRollbackToSavepoint
+        * @covers \Wikimedia\Rdbms\Database::startAtomic
+        * @covers \Wikimedia\Rdbms\Database::endAtomic
+        * @covers \Wikimedia\Rdbms\Database::cancelAtomic
+        * @covers \Wikimedia\Rdbms\Database::doAtomicSection
+        */
+       public function testAtomicSectionsTrxRound() {
+               $this->database->setFlag( IDatabase::DBO_TRX );
+               $this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
+               $this->database->query( 'SELECT 1', __METHOD__ );
+               $this->database->endAtomic( __METHOD__ );
+               $this->database->commit( __METHOD__, IDatabase::FLUSHING_ALL_PEERS );
+               // phpcs:ignore Generic.Files.LineLength
+               $this->assertLastSql( 'BEGIN; SAVEPOINT wikimedia_rdbms_atomic1; SELECT 1; RELEASE SAVEPOINT wikimedia_rdbms_atomic1; COMMIT' );
+       }
+
+       public static function provideAtomicSectionMethodsForErrors() {
+               return [
+                       [ 'endAtomic' ],
+                       [ 'cancelAtomic' ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideAtomicSectionMethodsForErrors
+        * @covers \Wikimedia\Rdbms\Database::endAtomic
+        * @covers \Wikimedia\Rdbms\Database::cancelAtomic
+        */
+       public function testNoAtomicSection( $method ) {
+               try {
+                       $this->database->$method( __METHOD__ );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( DBUnexpectedError $ex ) {
+                       $this->assertSame(
+                               'No atomic section is open (got ' . __METHOD__ . ').',
+                               $ex->getMessage()
+                       );
+               }
+       }
+
+       /**
+        * @dataProvider provideAtomicSectionMethodsForErrors
+        * @covers \Wikimedia\Rdbms\Database::endAtomic
+        * @covers \Wikimedia\Rdbms\Database::cancelAtomic
+        */
+       public function testInvalidAtomicSectionEnded( $method ) {
+               $this->database->startAtomic( __METHOD__ . 'X' );
+               try {
+                       $this->database->$method( __METHOD__ );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( DBUnexpectedError $ex ) {
+                       $this->assertSame(
+                               'Invalid atomic section ended (got ' . __METHOD__ . ' but expected ' .
+                                       __METHOD__ . 'X).',
+                               $ex->getMessage()
+                       );
+               }
+       }
+
+       /**
+        * @covers \Wikimedia\Rdbms\Database::cancelAtomic
+        */
+       public function testUncancellableAtomicSection() {
+               $this->database->startAtomic( __METHOD__ );
+               try {
+                       $this->database->cancelAtomic( __METHOD__ );
+                       $this->database->select( 'test', '1', [], __METHOD__ );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( DBTransactionError $ex ) {
+                       $this->assertSame(
+                               'Cannot execute query from ' . __METHOD__ . ' while transaction status is ERROR.',
+                               $ex->getMessage()
+                       );
+               }
+       }
+
+       /**
+        * @expectedException \Wikimedia\Rdbms\DBTransactionStateError
+        * @covers \Wikimedia\Rdbms\Database::assertQueryIsCurrentlyAllowed
+        */
+       public function testTransactionErrorState1() {
+               $wrapper = TestingAccessWrapper::newFromObject( $this->database );
+
+               $this->database->begin( __METHOD__ );
+               $wrapper->trxStatus = Database::STATUS_TRX_ERROR;
+               $this->database->delete( 'x', [ 'field' => 3 ], __METHOD__ );
+               $this->database->commit( __METHOD__ );
+       }
+
+       /**
+        * @covers \Wikimedia\Rdbms\Database::query
+        */
+       public function testTransactionErrorState2() {
+               $wrapper = TestingAccessWrapper::newFromObject( $this->database );
+
+               $this->database->startAtomic( __METHOD__ );
+               $wrapper->trxStatus = Database::STATUS_TRX_ERROR;
+               $this->database->rollback( __METHOD__ );
+               $this->assertEquals( 0, $this->database->trxLevel() );
+               $this->assertEquals( Database::STATUS_TRX_NONE, $wrapper->trxStatus() );
+               $this->assertLastSql( 'BEGIN; ROLLBACK' );
+
+               $this->database->startAtomic( __METHOD__ );
+               $this->assertEquals( Database::STATUS_TRX_OK, $wrapper->trxStatus() );
+               $this->database->delete( 'x', [ 'field' => 1 ], __METHOD__ );
+               $this->database->endAtomic( __METHOD__ );
+               $this->assertEquals( Database::STATUS_TRX_NONE, $wrapper->trxStatus() );
+               $this->assertLastSql( 'BEGIN; DELETE FROM x WHERE field = \'1\'; COMMIT' );
+               $this->assertEquals( 0, $this->database->trxLevel(), 'Use after rollback()' );
+
+               $this->database->begin( __METHOD__ );
+               $this->database->startAtomic( __METHOD__, Database::ATOMIC_CANCELABLE );
+               $this->database->update( 'y', [ 'a' => 1 ], [ 'field' => 1 ], __METHOD__ );
+               $wrapper->trxStatus = Database::STATUS_TRX_ERROR;
+               $this->database->cancelAtomic( __METHOD__ );
+               $this->assertEquals( Database::STATUS_TRX_OK, $wrapper->trxStatus() );
+               $this->database->startAtomic( __METHOD__ );
+               $this->database->delete( 'y', [ 'field' => 1 ], __METHOD__ );
+               $this->database->endAtomic( __METHOD__ );
+               $this->database->commit( __METHOD__ );
+               // phpcs:ignore Generic.Files.LineLength
+               $this->assertLastSql( 'BEGIN; SAVEPOINT wikimedia_rdbms_atomic1; UPDATE y SET a = \'1\' WHERE field = \'1\'; ROLLBACK TO SAVEPOINT wikimedia_rdbms_atomic1; DELETE FROM y WHERE field = \'1\'; COMMIT' );
+               $this->assertEquals( 0, $this->database->trxLevel(), 'Use after rollback()' );
+
+               // Next transaction
+               $this->database->startAtomic( __METHOD__ );
+               $this->assertEquals( Database::STATUS_TRX_OK, $wrapper->trxStatus() );
+               $this->database->delete( 'x', [ 'field' => 3 ], __METHOD__ );
+               $this->database->endAtomic( __METHOD__ );
+               $this->assertEquals( Database::STATUS_TRX_NONE, $wrapper->trxStatus() );
+               $this->assertLastSql( 'BEGIN; DELETE FROM x WHERE field = \'3\'; COMMIT' );
+               $this->assertEquals( 0, $this->database->trxLevel() );
+       }
+
+       /**
+        * @covers \Wikimedia\Rdbms\Database::query
+        */
+       public function testImplicitTransactionRollback() {
+               $doError = function () {
+                       $this->database->forceNextQueryError( 666, 'Evilness' );
+                       try {
+                               $this->database->delete( 'error', '1', __CLASS__ . '::SomeCaller' );
+                               $this->fail( 'Expected exception not thrown' );
+                       } catch ( DBError $e ) {
+                               $this->assertSame( 666, $e->errno );
+                       }
+               };
+
+               $this->database->setFlag( Database::DBO_TRX );
+
+               // Implicit transaction does not get silently rolled back
+               $this->database->begin( __METHOD__, Database::TRANSACTION_INTERNAL );
+               call_user_func( $doError );
+               try {
+                       $this->database->delete( 'x', [ 'field' => 1 ], __METHOD__ );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( DBTransactionError $e ) {
+                       $this->assertEquals(
+                               'Cannot execute query from ' . __METHOD__ . ' while transaction status is ERROR.',
+                               $e->getMessage()
+                       );
+               }
+               try {
+                       $this->database->commit( __METHOD__, Database::FLUSHING_INTERNAL );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( DBTransactionError $e ) {
+                       $this->assertEquals(
+                               'Cannot execute query from ' . __METHOD__ . ' while transaction status is ERROR.',
+                               $e->getMessage()
+                       );
+               }
+               $this->database->rollback( __METHOD__, Database::FLUSHING_INTERNAL );
+               $this->assertLastSql( 'BEGIN; DELETE FROM error WHERE 1; ROLLBACK' );
+
+               // Likewise if there were prior writes
+               $this->database->begin( __METHOD__, Database::TRANSACTION_INTERNAL );
+               $this->database->delete( 'x', [ 'field' => 1 ], __METHOD__ );
+               call_user_func( $doError );
+               try {
+                       $this->database->delete( 'x', [ 'field' => 1 ], __METHOD__ );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( DBTransactionStateError $e ) {
+               }
+               $this->database->rollback( __METHOD__, Database::FLUSHING_INTERNAL );
+               // phpcs:ignore
+               $this->assertLastSql( 'BEGIN; DELETE FROM x WHERE field = \'1\'; DELETE FROM error WHERE 1; ROLLBACK' );
+       }
+
+       /**
+        * @covers \Wikimedia\Rdbms\Database::query
+        */
+       public function testTransactionStatementRollbackIgnoring() {
+               $wrapper = TestingAccessWrapper::newFromObject( $this->database );
+               $warning = [];
+               $wrapper->deprecationLogger = function ( $msg ) use ( &$warning ) {
+                       $warning[] = $msg;
+               };
+
+               $doError = function () {
+                       $this->database->forceNextQueryError( 666, 'Evilness', [
+                               'wasKnownStatementRollbackError' => true,
+                       ] );
+                       try {
+                               $this->database->delete( 'error', '1', __CLASS__ . '::SomeCaller' );
+                               $this->fail( 'Expected exception not thrown' );
+                       } catch ( DBError $e ) {
+                               $this->assertSame( 666, $e->errno );
+                       }
+               };
+               $expectWarning = 'Caller from ' . __METHOD__ .
+                       ' ignored an error originally raised from ' . __CLASS__ . '::SomeCaller: [666] Evilness';
+
+               // Rollback doesn't raise a warning
+               $warning = [];
+               $this->database->startAtomic( __METHOD__ );
+               call_user_func( $doError );
+               $this->database->rollback( __METHOD__ );
+               $this->database->delete( 'x', [ 'field' => 1 ], __METHOD__ );
+               $this->assertSame( [], $warning );
+               // phpcs:ignore
+               $this->assertLastSql( 'BEGIN; DELETE FROM error WHERE 1; ROLLBACK; DELETE FROM x WHERE field = \'1\'' );
+
+               // cancelAtomic() doesn't raise a warning
+               $warning = [];
+               $this->database->begin( __METHOD__ );
+               $this->database->startAtomic( __METHOD__, Database::ATOMIC_CANCELABLE );
+               call_user_func( $doError );
+               $this->database->cancelAtomic( __METHOD__ );
+               $this->database->delete( 'x', [ 'field' => 1 ], __METHOD__ );
+               $this->database->commit( __METHOD__ );
+               $this->assertSame( [], $warning );
+               // phpcs:ignore
+               $this->assertLastSql( 'BEGIN; SAVEPOINT wikimedia_rdbms_atomic1; DELETE FROM error WHERE 1; ROLLBACK TO SAVEPOINT wikimedia_rdbms_atomic1; DELETE FROM x WHERE field = \'1\'; COMMIT' );
+
+               // Commit does raise a warning
+               $warning = [];
+               $this->database->begin( __METHOD__ );
+               call_user_func( $doError );
+               $this->database->commit( __METHOD__ );
+               $this->assertSame( [ $expectWarning ], $warning );
+               $this->assertLastSql( 'BEGIN; DELETE FROM error WHERE 1; COMMIT' );
+
+               // Deprecation only gets raised once
+               $warning = [];
+               $this->database->begin( __METHOD__ );
+               call_user_func( $doError );
+               $this->database->delete( 'x', [ 'field' => 1 ], __METHOD__ );
+               $this->database->commit( __METHOD__ );
+               $this->assertSame( [ $expectWarning ], $warning );
+               // phpcs:ignore
+               $this->assertLastSql( 'BEGIN; DELETE FROM error WHERE 1; DELETE FROM x WHERE field = \'1\'; COMMIT' );
+       }
+
+       /**
+        * @covers \Wikimedia\Rdbms\Database::close
+        */
+       public function testPrematureClose1() {
+               $fname = __METHOD__;
+               $this->database->begin( __METHOD__ );
+               $this->database->onTransactionCommitOrIdle( function () use ( $fname ) {
+                       $this->database->query( 'SELECT 1', $fname );
+               } );
+               $this->database->onTransactionResolution( function () use ( $fname ) {
+                       $this->database->query( 'SELECT 2', $fname );
+               } );
+               $this->database->delete( 'x', [ 'field' => 3 ], __METHOD__ );
+               try {
+                       $this->database->close();
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( DBUnexpectedError $ex ) {
+                       $this->assertSame(
+                               "Wikimedia\Rdbms\Database::close: transaction is still open (from $fname).",
+                               $ex->getMessage()
+                       );
+               }
+
+               $this->assertFalse( $this->database->isOpen() );
+               $this->assertLastSql( 'BEGIN; DELETE FROM x WHERE field = \'3\'; ROLLBACK; SELECT 2' );
+               $this->assertEquals( 0, $this->database->trxLevel() );
+       }
+
+       /**
+        * @covers \Wikimedia\Rdbms\Database::close
+        */
+       public function testPrematureClose2() {
+               try {
+                       $fname = __METHOD__;
+                       $this->database->startAtomic( __METHOD__ );
+                       $this->database->onTransactionCommitOrIdle( function () use ( $fname ) {
+                               $this->database->query( 'SELECT 1', $fname );
+                       } );
+                       $this->database->delete( 'x', [ 'field' => 3 ], __METHOD__ );
+                       $this->database->close();
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( DBUnexpectedError $ex ) {
+                       $this->assertSame(
+                               'Wikimedia\Rdbms\Database::close: atomic sections ' .
+                               'DatabaseSQLTest::testPrematureClose2 are still open.',
+                               $ex->getMessage()
+                       );
+               }
+
+               $this->assertFalse( $this->database->isOpen() );
+               $this->assertLastSql( 'BEGIN; DELETE FROM x WHERE field = \'3\'; ROLLBACK' );
+               $this->assertEquals( 0, $this->database->trxLevel() );
+       }
+
+       /**
+        * @covers \Wikimedia\Rdbms\Database::close
+        */
+       public function testPrematureClose3() {
+               try {
+                       $this->database->setFlag( IDatabase::DBO_TRX );
+                       $this->database->delete( 'x', [ 'field' => 3 ], __METHOD__ );
+                       $this->assertEquals( 1, $this->database->trxLevel() );
+                       $this->database->close();
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( DBUnexpectedError $ex ) {
+                       $this->assertSame(
+                               'Wikimedia\Rdbms\Database::close: ' .
+                               'mass commit/rollback of peer transaction required (DBO_TRX set).',
+                               $ex->getMessage()
+                       );
+               }
+
+               $this->assertFalse( $this->database->isOpen() );
+               $this->assertLastSql( 'BEGIN; DELETE FROM x WHERE field = \'3\'; ROLLBACK' );
+               $this->assertEquals( 0, $this->database->trxLevel() );
+       }
+
+       /**
+        * @covers \Wikimedia\Rdbms\Database::close
+        */
+       public function testPrematureClose4() {
+               $this->database->setFlag( IDatabase::DBO_TRX );
+               $this->database->query( 'SELECT 1', __METHOD__ );
+               $this->assertEquals( 1, $this->database->trxLevel() );
+               $this->database->close();
+               $this->database->clearFlag( IDatabase::DBO_TRX );
+
+               $this->assertFalse( $this->database->isOpen() );
+               $this->assertLastSql( 'BEGIN; SELECT 1; ROLLBACK' );
+               $this->assertEquals( 0, $this->database->trxLevel() );
+       }
+
+       /**
+        * @covers Wikimedia\Rdbms\Database::selectFieldValues()
+        */
+       public function testSelectFieldValues() {
+               $this->database->forceNextResult( [
+                       (object)[ 'value' => 'row1' ],
+                       (object)[ 'value' => 'row2' ],
+                       (object)[ 'value' => 'row3' ],
+               ] );
+
+               $this->assertSame(
+                       [ 'row1', 'row2', 'row3' ],
+                       $this->database->selectFieldValues( 'table', 'table.field', 'conds', __METHOD__ )
+               );
+               $this->assertLastSql( 'SELECT table.field AS value FROM table WHERE conds' );
+       }
+}
diff --git a/tests/phpunit/unit/includes/libs/rdbms/database/DatabaseSqliteRdbmsTest.php b/tests/phpunit/unit/includes/libs/rdbms/database/DatabaseSqliteRdbmsTest.php
new file mode 100644 (file)
index 0000000..a886d6b
--- /dev/null
@@ -0,0 +1,60 @@
+<?php
+
+use Wikimedia\Rdbms\DatabaseSqlite;
+
+/**
+ * DatabaseSqliteTest is already defined in mediawiki core hence the 'Rdbms' included in this
+ * class name.
+ * The test in core should have mediawiki specific stuff removed and the tests moved to this
+ * rdbms libs test.
+ */
+class DatabaseSqliteRdbmsTest extends PHPUnit\Framework\TestCase {
+
+       use MediaWikiCoversValidator;
+       use PHPUnit4And6Compat;
+
+       /**
+        * @return PHPUnit_Framework_MockObject_MockObject|DatabaseSqlite
+        */
+       private function getMockDb() {
+               return $this->getMockBuilder( DatabaseSqlite::class )
+                       ->disableOriginalConstructor()
+                       ->setMethods( null )
+                       ->getMock();
+       }
+
+       public function provideBuildSubstring() {
+               yield [ 'someField', 1, 2, 'SUBSTR(someField,1,2)' ];
+               yield [ 'someField', 1, null, 'SUBSTR(someField,1)' ];
+       }
+
+       /**
+        * @covers Wikimedia\Rdbms\DatabaseSqlite::buildSubstring
+        * @dataProvider provideBuildSubstring
+        */
+       public function testBuildSubstring( $input, $start, $length, $expected ) {
+               $dbMock = $this->getMockDb();
+               $output = $dbMock->buildSubstring( $input, $start, $length );
+               $this->assertSame( $expected, $output );
+       }
+
+       public function provideBuildSubstring_invalidParams() {
+               yield [ -1, 1 ];
+               yield [ 1, -1 ];
+               yield [ 1, 'foo' ];
+               yield [ 'foo', 1 ];
+               yield [ null, 1 ];
+               yield [ 0, 1 ];
+       }
+
+       /**
+        * @covers Wikimedia\Rdbms\DatabaseSqlite::buildSubstring
+        * @dataProvider provideBuildSubstring_invalidParams
+        */
+       public function testBuildSubstring_invalidParams( $start, $length ) {
+               $dbMock = $this->getMockDb();
+               $this->setExpectedException( InvalidArgumentException::class );
+               $dbMock->buildSubstring( 'foo', $start, $length );
+       }
+
+}
diff --git a/tests/phpunit/unit/includes/libs/rdbms/database/DatabaseTest.php b/tests/phpunit/unit/includes/libs/rdbms/database/DatabaseTest.php
new file mode 100644 (file)
index 0000000..8b24791
--- /dev/null
@@ -0,0 +1,707 @@
+<?php
+
+use Wikimedia\Rdbms\Database;
+use Wikimedia\Rdbms\IDatabase;
+use Wikimedia\Rdbms\DatabaseDomain;
+use Wikimedia\Rdbms\DatabaseMysqli;
+use Wikimedia\Rdbms\LBFactorySingle;
+use Wikimedia\Rdbms\TransactionProfiler;
+use Wikimedia\TestingAccessWrapper;
+use Wikimedia\Rdbms\DatabaseSqlite;
+use Wikimedia\Rdbms\DatabasePostgres;
+use Wikimedia\Rdbms\DatabaseMssql;
+use Wikimedia\Rdbms\DBUnexpectedError;
+
+class DatabaseTest extends PHPUnit\Framework\TestCase {
+       /** @var DatabaseTestHelper */
+       private $db;
+
+       use MediaWikiCoversValidator;
+
+       protected function setUp() {
+               $this->db = new DatabaseTestHelper( __CLASS__ . '::' . $this->getName() );
+       }
+
+       /**
+        * @dataProvider provideAddQuotes
+        * @covers Wikimedia\Rdbms\Database::factory
+        */
+       public function testFactory() {
+               $m = Database::NEW_UNCONNECTED; // no-connect mode
+               $p = [ 'host' => 'localhost', 'user' => 'me', 'password' => 'myself', 'dbname' => 'i' ];
+
+               $this->assertInstanceOf( DatabaseMysqli::class, Database::factory( 'mysqli', $p, $m ) );
+               $this->assertInstanceOf( DatabaseMysqli::class, Database::factory( 'MySqli', $p, $m ) );
+               $this->assertInstanceOf( DatabaseMysqli::class, Database::factory( 'MySQLi', $p, $m ) );
+               $this->assertInstanceOf( DatabasePostgres::class, Database::factory( 'postgres', $p, $m ) );
+               $this->assertInstanceOf( DatabasePostgres::class, Database::factory( 'Postgres', $p, $m ) );
+
+               $x = $p + [ 'port' => 10000, 'UseWindowsAuth' => false ];
+               $this->assertInstanceOf( DatabaseMssql::class, Database::factory( 'mssql', $x, $m ) );
+
+               $x = $p + [ 'dbFilePath' => 'some/file.sqlite' ];
+               $this->assertInstanceOf( DatabaseSqlite::class, Database::factory( 'sqlite', $x, $m ) );
+               $x = $p + [ 'dbDirectory' => 'some/file' ];
+               $this->assertInstanceOf( DatabaseSqlite::class, Database::factory( 'sqlite', $x, $m ) );
+       }
+
+       public static function provideAddQuotes() {
+               return [
+                       [ null, 'NULL' ],
+                       [ 1234, "'1234'" ],
+                       [ 1234.5678, "'1234.5678'" ],
+                       [ 'string', "'string'" ],
+                       [ 'string\'s cause trouble', "'string\'s cause trouble'" ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideAddQuotes
+        * @covers Wikimedia\Rdbms\Database::addQuotes
+        */
+       public function testAddQuotes( $input, $expected ) {
+               $this->assertEquals( $expected, $this->db->addQuotes( $input ) );
+       }
+
+       public static function provideTableName() {
+               // Formatting is mostly ignored since addIdentifierQuotes is abstract.
+               // For testing of addIdentifierQuotes, see actual Database subclas tests.
+               return [
+                       'local' => [
+                               'tablename',
+                               'tablename',
+                               'quoted',
+                       ],
+                       'local-raw' => [
+                               'tablename',
+                               'tablename',
+                               'raw',
+                       ],
+                       'shared' => [
+                               'sharedb.tablename',
+                               'tablename',
+                               'quoted',
+                               [ 'dbname' => 'sharedb', 'schema' => null, 'prefix' => '' ],
+                       ],
+                       'shared-raw' => [
+                               'sharedb.tablename',
+                               'tablename',
+                               'raw',
+                               [ 'dbname' => 'sharedb', 'schema' => null, 'prefix' => '' ],
+                       ],
+                       'shared-prefix' => [
+                               'sharedb.sh_tablename',
+                               'tablename',
+                               'quoted',
+                               [ 'dbname' => 'sharedb', 'schema' => null, 'prefix' => 'sh_' ],
+                       ],
+                       'shared-prefix-raw' => [
+                               'sharedb.sh_tablename',
+                               'tablename',
+                               'raw',
+                               [ 'dbname' => 'sharedb', 'schema' => null, 'prefix' => 'sh_' ],
+                       ],
+                       'foreign' => [
+                               'databasename.tablename',
+                               'databasename.tablename',
+                               'quoted',
+                       ],
+                       'foreign-raw' => [
+                               'databasename.tablename',
+                               'databasename.tablename',
+                               'raw',
+                       ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideTableName
+        * @covers Wikimedia\Rdbms\Database::tableName
+        */
+       public function testTableName( $expected, $table, $format, array $alias = null ) {
+               if ( $alias ) {
+                       $this->db->setTableAliases( [ $table => $alias ] );
+               }
+               $this->assertEquals(
+                       $expected,
+                       $this->db->tableName( $table, $format ?: 'quoted' )
+               );
+       }
+
+       public function provideTableNamesWithIndexClauseOrJOIN() {
+               return [
+                       'one-element array' => [
+                               [ 'table' ], [], 'table '
+                       ],
+                       'comma join' => [
+                               [ 'table1', 'table2' ], [], 'table1,table2 '
+                       ],
+                       'real join' => [
+                               [ 'table1', 'table2' ],
+                               [ 'table2' => [ 'LEFT JOIN', 't1_id = t2_id' ] ],
+                               'table1 LEFT JOIN table2 ON ((t1_id = t2_id))'
+                       ],
+                       'real join with multiple conditionals' => [
+                               [ 'table1', 'table2' ],
+                               [ 'table2' => [ 'LEFT JOIN', [ 't1_id = t2_id', 't2_x = \'X\'' ] ] ],
+                               'table1 LEFT JOIN table2 ON ((t1_id = t2_id) AND (t2_x = \'X\'))'
+                       ],
+                       'join with parenthesized group' => [
+                               [ 'table1', 'n' => [ 'table2', 'table3' ] ],
+                               [
+                                       'table3' => [ 'JOIN', 't2_id = t3_id' ],
+                                       'n' => [ 'LEFT JOIN', 't1_id = t2_id' ],
+                               ],
+                               'table1 LEFT JOIN (table2 JOIN table3 ON ((t2_id = t3_id))) ON ((t1_id = t2_id))'
+                       ],
+                       'join with degenerate parenthesized group' => [
+                               [ 'table1', 'n' => [ 't2' => 'table2' ] ],
+                               [
+                                       'n' => [ 'LEFT JOIN', 't1_id = t2_id' ],
+                               ],
+                               'table1 LEFT JOIN table2 t2 ON ((t1_id = t2_id))'
+                       ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideTableNamesWithIndexClauseOrJOIN
+        * @covers Wikimedia\Rdbms\Database::tableNamesWithIndexClauseOrJOIN
+        */
+       public function testTableNamesWithIndexClauseOrJOIN( $tables, $join_conds, $expect ) {
+               $clause = TestingAccessWrapper::newFromObject( $this->db )
+                       ->tableNamesWithIndexClauseOrJOIN( $tables, [], [], $join_conds );
+               $this->assertSame( $expect, $clause );
+       }
+
+       /**
+        * @covers Wikimedia\Rdbms\Database::onTransactionCommitOrIdle
+        * @covers Wikimedia\Rdbms\Database::runOnTransactionIdleCallbacks
+        */
+       public function testTransactionIdle() {
+               $db = $this->db;
+
+               $db->clearFlag( DBO_TRX );
+               $called = false;
+               $flagSet = null;
+               $callback = function ( $trigger, IDatabase $db ) use ( &$flagSet, &$called ) {
+                       $called = true;
+                       $flagSet = $db->getFlag( DBO_TRX );
+               };
+
+               $db->onTransactionCommitOrIdle( $callback, __METHOD__ );
+               $this->assertTrue( $called, 'Callback reached' );
+               $this->assertFalse( $flagSet, 'DBO_TRX off in callback' );
+               $this->assertFalse( $db->getFlag( DBO_TRX ), 'DBO_TRX still default' );
+
+               $flagSet = null;
+               $called = false;
+               $db->startAtomic( __METHOD__ );
+               $db->onTransactionCommitOrIdle( $callback, __METHOD__ );
+               $this->assertFalse( $called, 'Callback not reached during TRX' );
+               $db->endAtomic( __METHOD__ );
+
+               $this->assertTrue( $called, 'Callback reached after COMMIT' );
+               $this->assertFalse( $flagSet, 'DBO_TRX off in callback' );
+               $this->assertFalse( $db->getFlag( DBO_TRX ), 'DBO_TRX restored to default' );
+
+               $db->clearFlag( DBO_TRX );
+               $db->onTransactionCommitOrIdle(
+                       function ( $trigger, IDatabase $db ) {
+                               $db->setFlag( DBO_TRX );
+                       },
+                       __METHOD__
+               );
+               $this->assertFalse( $db->getFlag( DBO_TRX ), 'DBO_TRX restored to default' );
+       }
+
+       /**
+        * @covers Wikimedia\Rdbms\Database::onTransactionCommitOrIdle
+        * @covers Wikimedia\Rdbms\Database::runOnTransactionIdleCallbacks
+        */
+       public function testTransactionIdle_TRX() {
+               $db = $this->getMockDB( [ 'isOpen', 'ping', 'getDBname' ] );
+               $db->method( 'isOpen' )->willReturn( true );
+               $db->method( 'ping' )->willReturn( true );
+               $db->method( 'getDBname' )->willReturn( '' );
+               $db->setFlag( DBO_TRX );
+
+               $lbFactory = LBFactorySingle::newFromConnection( $db );
+               // Ask for the connection so that LB sets internal state
+               // about this connection being the master connection
+               $lb = $lbFactory->getMainLB();
+               $conn = $lb->openConnection( $lb->getWriterIndex() );
+               $this->assertSame( $db, $conn, 'Same DB instance' );
+               $this->assertTrue( $db->getFlag( DBO_TRX ), 'DBO_TRX is set' );
+
+               $called = false;
+               $flagSet = null;
+               $callback = function () use ( $db, &$flagSet, &$called ) {
+                       $called = true;
+                       $flagSet = $db->getFlag( DBO_TRX );
+               };
+
+               $db->onTransactionCommitOrIdle( $callback, __METHOD__ );
+               $this->assertTrue( $called, 'Called when idle if DBO_TRX is set' );
+               $this->assertFalse( $flagSet, 'DBO_TRX off in callback' );
+               $this->assertTrue( $db->getFlag( DBO_TRX ), 'DBO_TRX still default' );
+
+               $called = false;
+               $lbFactory->beginMasterChanges( __METHOD__ );
+               $db->onTransactionCommitOrIdle( $callback, __METHOD__ );
+               $this->assertFalse( $called, 'Not called when lb-transaction is active' );
+
+               $lbFactory->commitMasterChanges( __METHOD__ );
+               $this->assertTrue( $called, 'Called when lb-transaction is committed' );
+
+               $called = false;
+               $lbFactory->beginMasterChanges( __METHOD__ );
+               $db->onTransactionCommitOrIdle( $callback, __METHOD__ );
+               $this->assertFalse( $called, 'Not called when lb-transaction is active' );
+
+               $lbFactory->rollbackMasterChanges( __METHOD__ );
+               $this->assertFalse( $called, 'Not called when lb-transaction is rolled back' );
+
+               $lbFactory->commitMasterChanges( __METHOD__ );
+               $this->assertFalse( $called, 'Not called in next round commit' );
+
+               $db->setFlag( DBO_TRX );
+               try {
+                       $db->onTransactionCommitOrIdle( function () {
+                               throw new RuntimeException( 'test' );
+                       } );
+                       $this->fail( "Exception not thrown" );
+               } catch ( RuntimeException $e ) {
+                       $this->assertTrue( $db->getFlag( DBO_TRX ) );
+               }
+       }
+
+       /**
+        * @covers Wikimedia\Rdbms\Database::onTransactionPreCommitOrIdle
+        * @covers Wikimedia\Rdbms\Database::runOnTransactionPreCommitCallbacks
+        */
+       public function testTransactionPreCommitOrIdle() {
+               $db = $this->getMockDB( [ 'isOpen' ] );
+               $db->method( 'isOpen' )->willReturn( true );
+               $db->clearFlag( DBO_TRX );
+
+               $this->assertFalse( $db->getFlag( DBO_TRX ), 'DBO_TRX is not set' );
+
+               $called = false;
+               $db->onTransactionPreCommitOrIdle(
+                       function ( IDatabase $db ) use ( &$called ) {
+                               $called = true;
+                       },
+                       __METHOD__
+               );
+               $this->assertTrue( $called, 'Called when idle' );
+
+               $db->begin( __METHOD__ );
+               $called = false;
+               $db->onTransactionPreCommitOrIdle(
+                       function ( IDatabase $db ) use ( &$called ) {
+                               $called = true;
+                       },
+                       __METHOD__
+               );
+               $this->assertFalse( $called, 'Not called when transaction is active' );
+               $db->commit( __METHOD__ );
+               $this->assertTrue( $called, 'Called when transaction is committed' );
+       }
+
+       /**
+        * @covers Wikimedia\Rdbms\Database::onTransactionPreCommitOrIdle
+        * @covers Wikimedia\Rdbms\Database::runOnTransactionPreCommitCallbacks
+        */
+       public function testTransactionPreCommitOrIdle_TRX() {
+               $db = $this->getMockDB( [ 'isOpen', 'ping', 'getDBname' ] );
+               $db->method( 'isOpen' )->willReturn( true );
+               $db->method( 'ping' )->willReturn( true );
+               $db->method( 'getDBname' )->willReturn( 'unittest' );
+               $db->setFlag( DBO_TRX );
+
+               $lbFactory = LBFactorySingle::newFromConnection( $db );
+               // Ask for the connection so that LB sets internal state
+               // about this connection being the master connection
+               $lb = $lbFactory->getMainLB();
+               $conn = $lb->openConnection( $lb->getWriterIndex() );
+               $this->assertSame( $db, $conn, 'Same DB instance' );
+
+               $this->assertFalse( $lb->hasMasterChanges() );
+               $this->assertTrue( $db->getFlag( DBO_TRX ), 'DBO_TRX is set' );
+               $called = false;
+               $callback = function ( IDatabase $db ) use ( &$called ) {
+                       $called = true;
+               };
+               $db->onTransactionPreCommitOrIdle( $callback, __METHOD__ );
+               $this->assertTrue( $called, 'Called when idle if DBO_TRX is set' );
+               $called = false;
+               $lbFactory->commitMasterChanges();
+               $this->assertFalse( $called );
+
+               $called = false;
+               $lbFactory->beginMasterChanges( __METHOD__ );
+               $db->onTransactionPreCommitOrIdle( $callback, __METHOD__ );
+               $this->assertFalse( $called, 'Not called when lb-transaction is active' );
+               $lbFactory->commitMasterChanges( __METHOD__ );
+               $this->assertTrue( $called, 'Called when lb-transaction is committed' );
+
+               $called = false;
+               $lbFactory->beginMasterChanges( __METHOD__ );
+               $db->onTransactionPreCommitOrIdle( $callback, __METHOD__ );
+               $this->assertFalse( $called, 'Not called when lb-transaction is active' );
+
+               $lbFactory->rollbackMasterChanges( __METHOD__ );
+               $this->assertFalse( $called, 'Not called when lb-transaction is rolled back' );
+
+               $lbFactory->commitMasterChanges( __METHOD__ );
+               $this->assertFalse( $called, 'Not called in next round commit' );
+       }
+
+       /**
+        * @covers Wikimedia\Rdbms\Database::onTransactionResolution
+        * @covers Wikimedia\Rdbms\Database::runOnTransactionIdleCallbacks
+        */
+       public function testTransactionResolution() {
+               $db = $this->db;
+
+               $db->clearFlag( DBO_TRX );
+               $db->begin( __METHOD__ );
+               $called = false;
+               $db->onTransactionResolution( function ( $trigger, IDatabase $db ) use ( &$called ) {
+                       $called = true;
+                       $db->setFlag( DBO_TRX );
+               } );
+               $db->commit( __METHOD__ );
+               $this->assertFalse( $db->getFlag( DBO_TRX ), 'DBO_TRX restored to default' );
+               $this->assertTrue( $called, 'Callback reached' );
+
+               $db->clearFlag( DBO_TRX );
+               $db->begin( __METHOD__ );
+               $called = false;
+               $db->onTransactionResolution( function ( $trigger, IDatabase $db ) use ( &$called ) {
+                       $called = true;
+                       $db->setFlag( DBO_TRX );
+               } );
+               $db->rollback( __METHOD__ );
+               $this->assertFalse( $db->getFlag( DBO_TRX ), 'DBO_TRX restored to default' );
+               $this->assertTrue( $called, 'Callback reached' );
+       }
+
+       /**
+        * @covers Wikimedia\Rdbms\Database::setTransactionListener
+        */
+       public function testTransactionListener() {
+               $db = $this->db;
+
+               $db->setTransactionListener( 'ping', function () use ( $db, &$called ) {
+                       $called = true;
+               } );
+
+               $called = false;
+               $db->begin( __METHOD__ );
+               $db->commit( __METHOD__ );
+               $this->assertTrue( $called, 'Callback reached' );
+
+               $called = false;
+               $db->begin( __METHOD__ );
+               $db->commit( __METHOD__ );
+               $this->assertTrue( $called, 'Callback still reached' );
+
+               $called = false;
+               $db->begin( __METHOD__ );
+               $db->rollback( __METHOD__ );
+               $this->assertTrue( $called, 'Callback reached' );
+
+               $db->setTransactionListener( 'ping', null );
+               $called = false;
+               $db->begin( __METHOD__ );
+               $db->commit( __METHOD__ );
+               $this->assertFalse( $called, 'Callback not reached' );
+       }
+
+       /**
+        * Use this mock instead of DatabaseTestHelper for cases where
+        * DatabaseTestHelper is too inflexibile due to mocking too much
+        * or being too restrictive about fname matching (e.g. for tests
+        * that assert behaviour when the name is a mismatch, we need to
+        * catch the error here instead of there).
+        *
+        * @return Database
+        */
+       private function getMockDB( $methods = [] ) {
+               static $abstractMethods = [
+                       'fetchAffectedRowCount',
+                       'closeConnection',
+                       'dataSeek',
+                       'doQuery',
+                       'fetchObject', 'fetchRow',
+                       'fieldInfo', 'fieldName',
+                       'getSoftwareLink', 'getServerVersion',
+                       'getType',
+                       'indexInfo',
+                       'insertId',
+                       'lastError', 'lastErrno',
+                       'numFields', 'numRows',
+                       'open',
+                       'strencode',
+                       'tableExists'
+               ];
+               $db = $this->getMockBuilder( Database::class )
+                       ->disableOriginalConstructor()
+                       ->setMethods( array_values( array_unique( array_merge(
+                               $abstractMethods,
+                               $methods
+                       ) ) ) )
+                       ->getMock();
+               $wdb = TestingAccessWrapper::newFromObject( $db );
+               $wdb->trxProfiler = new TransactionProfiler();
+               $wdb->connLogger = new \Psr\Log\NullLogger();
+               $wdb->queryLogger = new \Psr\Log\NullLogger();
+               $wdb->currentDomain = DatabaseDomain::newUnspecified();
+               return $db;
+       }
+
+       /**
+        * @covers Wikimedia\Rdbms\Database::flushSnapshot
+        */
+       public function testFlushSnapshot() {
+               $db = $this->getMockDB( [ 'isOpen' ] );
+               $db->method( 'isOpen' )->willReturn( true );
+
+               $db->flushSnapshot( __METHOD__ ); // ok
+               $db->flushSnapshot( __METHOD__ ); // ok
+
+               $db->setFlag( DBO_TRX, $db::REMEMBER_PRIOR );
+               $db->query( 'SELECT 1', __METHOD__ );
+               $this->assertTrue( (bool)$db->trxLevel(), "Transaction started." );
+               $db->flushSnapshot( __METHOD__ ); // ok
+               $db->restoreFlags( $db::RESTORE_PRIOR );
+
+               $this->assertFalse( (bool)$db->trxLevel(), "Transaction cleared." );
+       }
+
+       /**
+        * @covers Wikimedia\Rdbms\Database::getScopedLockAndFlush
+        * @covers Wikimedia\Rdbms\Database::lock
+        * @covers Wikimedia\Rdbms\Database::unlock
+        * @covers Wikimedia\Rdbms\Database::lockIsFree
+        */
+       public function testGetScopedLock() {
+               $db = $this->getMockDB( [ 'isOpen', 'getDBname' ] );
+               $db->method( 'isOpen' )->willReturn( true );
+               $db->method( 'getDBname' )->willReturn( 'unittest' );
+
+               $this->assertEquals( 0, $db->trxLevel() );
+               $this->assertEquals( true, $db->lockIsFree( 'x', __METHOD__ ) );
+               $this->assertEquals( true, $db->lock( 'x', __METHOD__ ) );
+               $this->assertEquals( false, $db->lockIsFree( 'x', __METHOD__ ) );
+               $this->assertEquals( true, $db->unlock( 'x', __METHOD__ ) );
+               $this->assertEquals( true, $db->lockIsFree( 'x', __METHOD__ ) );
+               $this->assertEquals( 0, $db->trxLevel() );
+
+               $db->setFlag( DBO_TRX );
+               $this->assertEquals( true, $db->lockIsFree( 'x', __METHOD__ ) );
+               $this->assertEquals( true, $db->lock( 'x', __METHOD__ ) );
+               $this->assertEquals( false, $db->lockIsFree( 'x', __METHOD__ ) );
+               $this->assertEquals( true, $db->unlock( 'x', __METHOD__ ) );
+               $this->assertEquals( true, $db->lockIsFree( 'x', __METHOD__ ) );
+               $db->clearFlag( DBO_TRX );
+
+               // Pending writes with DBO_TRX
+               $this->assertEquals( 0, $db->trxLevel() );
+               $this->assertTrue( $db->lockIsFree( 'meow', __METHOD__ ) );
+               $db->setFlag( DBO_TRX );
+               $db->query( "DELETE FROM test WHERE t = 1" ); // trigger DBO_TRX transaction before lock
+               try {
+                       $lock = $db->getScopedLockAndFlush( 'meow', __METHOD__, 1 );
+                       $this->fail( "Exception not reached" );
+               } catch ( DBUnexpectedError $e ) {
+                       $this->assertEquals( 1, $db->trxLevel(), "Transaction not committed." );
+                       $this->assertTrue( $db->lockIsFree( 'meow', __METHOD__ ), 'Lock not acquired' );
+               }
+               $db->rollback( __METHOD__, IDatabase::FLUSHING_ALL_PEERS );
+               // Pending writes without DBO_TRX
+               $db->clearFlag( DBO_TRX );
+               $this->assertEquals( 0, $db->trxLevel() );
+               $this->assertTrue( $db->lockIsFree( 'meow2', __METHOD__ ) );
+               $db->begin( __METHOD__ );
+               $db->query( "DELETE FROM test WHERE t = 1" ); // trigger DBO_TRX transaction before lock
+               try {
+                       $lock = $db->getScopedLockAndFlush( 'meow2', __METHOD__, 1 );
+                       $this->fail( "Exception not reached" );
+               } catch ( DBUnexpectedError $e ) {
+                       $this->assertEquals( 1, $db->trxLevel(), "Transaction not committed." );
+                       $this->assertTrue( $db->lockIsFree( 'meow2', __METHOD__ ), 'Lock not acquired' );
+               }
+               $db->rollback( __METHOD__ );
+               // No pending writes, with DBO_TRX
+               $db->setFlag( DBO_TRX );
+               $this->assertEquals( 0, $db->trxLevel() );
+               $this->assertTrue( $db->lockIsFree( 'wuff', __METHOD__ ) );
+               $db->query( "SELECT 1", __METHOD__ );
+               $this->assertEquals( 1, $db->trxLevel() );
+               $lock = $db->getScopedLockAndFlush( 'wuff', __METHOD__, 1 );
+               $this->assertEquals( 0, $db->trxLevel() );
+               $this->assertFalse( $db->lockIsFree( 'wuff', __METHOD__ ), 'Lock already acquired' );
+               $db->rollback( __METHOD__, IDatabase::FLUSHING_ALL_PEERS );
+               // No pending writes, without DBO_TRX
+               $db->clearFlag( DBO_TRX );
+               $this->assertEquals( 0, $db->trxLevel() );
+               $this->assertTrue( $db->lockIsFree( 'wuff2', __METHOD__ ) );
+               $db->begin( __METHOD__ );
+               try {
+                       $lock = $db->getScopedLockAndFlush( 'wuff2', __METHOD__, 1 );
+                       $this->fail( "Exception not reached" );
+               } catch ( DBUnexpectedError $e ) {
+                       $this->assertEquals( 1, $db->trxLevel(), "Transaction not committed." );
+                       $this->assertFalse( $db->lockIsFree( 'wuff2', __METHOD__ ), 'Lock not acquired' );
+               }
+               $db->rollback( __METHOD__ );
+       }
+
+       /**
+        * @covers Wikimedia\Rdbms\Database::getFlag
+        * @covers Wikimedia\Rdbms\Database::setFlag
+        * @covers Wikimedia\Rdbms\Database::restoreFlags
+        */
+       public function testFlagSetting() {
+               $db = $this->db;
+               $origTrx = $db->getFlag( DBO_TRX );
+               $origSsl = $db->getFlag( DBO_SSL );
+
+               $origTrx
+                       ? $db->clearFlag( DBO_TRX, $db::REMEMBER_PRIOR )
+                       : $db->setFlag( DBO_TRX, $db::REMEMBER_PRIOR );
+               $this->assertEquals( !$origTrx, $db->getFlag( DBO_TRX ) );
+
+               $origSsl
+                       ? $db->clearFlag( DBO_SSL, $db::REMEMBER_PRIOR )
+                       : $db->setFlag( DBO_SSL, $db::REMEMBER_PRIOR );
+               $this->assertEquals( !$origSsl, $db->getFlag( DBO_SSL ) );
+
+               $db->restoreFlags( $db::RESTORE_INITIAL );
+               $this->assertEquals( $origTrx, $db->getFlag( DBO_TRX ) );
+               $this->assertEquals( $origSsl, $db->getFlag( DBO_SSL ) );
+
+               $origTrx
+                       ? $db->clearFlag( DBO_TRX, $db::REMEMBER_PRIOR )
+                       : $db->setFlag( DBO_TRX, $db::REMEMBER_PRIOR );
+               $origSsl
+                       ? $db->clearFlag( DBO_SSL, $db::REMEMBER_PRIOR )
+                       : $db->setFlag( DBO_SSL, $db::REMEMBER_PRIOR );
+
+               $db->restoreFlags();
+               $this->assertEquals( $origSsl, $db->getFlag( DBO_SSL ) );
+               $this->assertEquals( !$origTrx, $db->getFlag( DBO_TRX ) );
+
+               $db->restoreFlags();
+               $this->assertEquals( $origSsl, $db->getFlag( DBO_SSL ) );
+               $this->assertEquals( $origTrx, $db->getFlag( DBO_TRX ) );
+       }
+
+       /**
+        * @expectedException UnexpectedValueException
+        * @covers Wikimedia\Rdbms\Database::setFlag
+        */
+       public function testDBOIgnoreSet() {
+               $db = $this->getMockBuilder( DatabaseMysqli::class )
+                       ->disableOriginalConstructor()
+                       ->setMethods( null )
+                       ->getMock();
+
+               $db->setFlag( Database::DBO_IGNORE );
+       }
+
+       /**
+        * @expectedException UnexpectedValueException
+        * @covers Wikimedia\Rdbms\Database::clearFlag
+        */
+       public function testDBOIgnoreClear() {
+               $db = $this->getMockBuilder( DatabaseMysqli::class )
+                       ->disableOriginalConstructor()
+                       ->setMethods( null )
+                       ->getMock();
+
+               $db->clearFlag( Database::DBO_IGNORE );
+       }
+
+       /**
+        * @covers Wikimedia\Rdbms\Database::tablePrefix
+        * @covers Wikimedia\Rdbms\Database::dbSchema
+        */
+       public function testSchemaAndPrefixMutators() {
+               $ud = DatabaseDomain::newUnspecified();
+
+               $this->assertEquals( $ud->getId(), $this->db->getDomainID() );
+
+               $old = $this->db->tablePrefix();
+               $oldDomain = $this->db->getDomainId();
+               $this->assertInternalType( 'string', $old, 'Prefix is string' );
+               $this->assertSame( $old, $this->db->tablePrefix(), "Prefix unchanged" );
+               $this->assertSame( $old, $this->db->tablePrefix( 'xxx_' ) );
+               $this->assertSame( 'xxx_', $this->db->tablePrefix(), "Prefix set" );
+               $this->db->tablePrefix( $old );
+               $this->assertNotEquals( 'xxx_', $this->db->tablePrefix() );
+               $this->assertSame( $oldDomain, $this->db->getDomainId() );
+
+               $old = $this->db->dbSchema();
+               $oldDomain = $this->db->getDomainId();
+               $this->assertInternalType( 'string', $old, 'Schema is string' );
+               $this->assertSame( $old, $this->db->dbSchema(), "Schema unchanged" );
+
+               $this->db->selectDB( 'y' );
+               $this->assertSame( $old, $this->db->dbSchema( 'xxx' ) );
+               $this->assertSame( 'xxx', $this->db->dbSchema(), "Schema set" );
+               $this->db->dbSchema( $old );
+               $this->assertNotEquals( 'xxx', $this->db->dbSchema() );
+               $this->assertSame( "y", $this->db->getDomainId() );
+       }
+
+       /**
+        * @covers Wikimedia\Rdbms\Database::tablePrefix
+        * @covers Wikimedia\Rdbms\Database::dbSchema
+        * @expectedException DBUnexpectedError
+        */
+       public function testSchemaWithNoDB() {
+               $ud = DatabaseDomain::newUnspecified();
+
+               $this->assertEquals( $ud->getId(), $this->db->getDomainID() );
+               $this->assertSame( '', $this->db->dbSchema() );
+
+               $this->db->dbSchema( 'xxx' );
+       }
+
+       /**
+        * @covers Wikimedia\Rdbms\Database::selectDomain
+        */
+       public function testSelectDomain() {
+               $oldDomain = $this->db->getDomainId();
+               $oldDatabase = $this->db->getDBname();
+               $oldSchema = $this->db->dbSchema();
+               $oldPrefix = $this->db->tablePrefix();
+
+               $this->db->selectDomain( 'testselectdb-xxx_' );
+               $this->assertSame( 'testselectdb', $this->db->getDBname() );
+               $this->assertSame( '', $this->db->dbSchema() );
+               $this->assertSame( 'xxx_', $this->db->tablePrefix() );
+
+               $this->db->selectDomain( $oldDomain );
+               $this->assertSame( $oldDatabase, $this->db->getDBname() );
+               $this->assertSame( $oldSchema, $this->db->dbSchema() );
+               $this->assertSame( $oldPrefix, $this->db->tablePrefix() );
+               $this->assertSame( $oldDomain, $this->db->getDomainId() );
+
+               $this->db->selectDomain( 'testselectdb-schema-xxx_' );
+               $this->assertSame( 'testselectdb', $this->db->getDBname() );
+               $this->assertSame( 'schema', $this->db->dbSchema() );
+               $this->assertSame( 'xxx_', $this->db->tablePrefix() );
+
+               $this->db->selectDomain( $oldDomain );
+               $this->assertSame( $oldDatabase, $this->db->getDBname() );
+               $this->assertSame( $oldSchema, $this->db->dbSchema() );
+               $this->assertSame( $oldPrefix, $this->db->tablePrefix() );
+               $this->assertSame( $oldDomain, $this->db->getDomainId() );
+       }
+
+}
diff --git a/tests/phpunit/unit/includes/libs/services/ServiceContainerTest.php b/tests/phpunit/unit/includes/libs/services/ServiceContainerTest.php
new file mode 100644 (file)
index 0000000..6e51883
--- /dev/null
@@ -0,0 +1,497 @@
+<?php
+
+use Wikimedia\Services\ServiceContainer;
+
+/**
+ * @covers Wikimedia\Services\ServiceContainer
+ */
+class ServiceContainerTest extends PHPUnit\Framework\TestCase {
+
+       use MediaWikiCoversValidator; // TODO this library is supposed to be independent of MediaWiki
+       use PHPUnit4And6Compat;
+
+       private function newServiceContainer( $extraArgs = [] ) {
+               return new ServiceContainer( $extraArgs );
+       }
+
+       public function testGetServiceNames() {
+               $services = $this->newServiceContainer();
+               $names = $services->getServiceNames();
+
+               $this->assertInternalType( 'array', $names );
+               $this->assertEmpty( $names );
+
+               $name = 'TestService92834576';
+               $services->defineService( $name, function () {
+                       return null;
+               } );
+
+               $names = $services->getServiceNames();
+               $this->assertContains( $name, $names );
+       }
+
+       public function testHasService() {
+               $services = $this->newServiceContainer();
+
+               $name = 'TestService92834576';
+               $this->assertFalse( $services->hasService( $name ) );
+
+               $services->defineService( $name, function () {
+                       return null;
+               } );
+
+               $this->assertTrue( $services->hasService( $name ) );
+       }
+
+       public function testGetService() {
+               $services = $this->newServiceContainer( [ 'Foo' ] );
+
+               $theService = new stdClass();
+               $name = 'TestService92834576';
+               $count = 0;
+
+               $services->defineService(
+                       $name,
+                       function ( $actualLocator, $extra ) use ( $services, $theService, &$count ) {
+                               $count++;
+                               PHPUnit_Framework_Assert::assertSame( $services, $actualLocator );
+                               PHPUnit_Framework_Assert::assertSame( $extra, 'Foo' );
+                               return $theService;
+                       }
+               );
+
+               $this->assertSame( $theService, $services->getService( $name ) );
+
+               $services->getService( $name );
+               $this->assertSame( 1, $count, 'instantiator should be called exactly once!' );
+       }
+
+       public function testGetService_fail_unknown() {
+               $services = $this->newServiceContainer();
+
+               $name = 'TestService92834576';
+
+               $this->setExpectedException( Wikimedia\Services\NoSuchServiceException::class );
+
+               $services->getService( $name );
+       }
+
+       public function testPeekService() {
+               $services = $this->newServiceContainer();
+
+               $services->defineService(
+                       'Foo',
+                       function () {
+                               return new stdClass();
+                       }
+               );
+
+               $services->defineService(
+                       'Bar',
+                       function () {
+                               return new stdClass();
+                       }
+               );
+
+               // trigger instantiation of Foo
+               $services->getService( 'Foo' );
+
+               $this->assertInternalType(
+                       'object',
+                       $services->peekService( 'Foo' ),
+                       'Peek should return the service object if it had been accessed before.'
+               );
+
+               $this->assertNull(
+                       $services->peekService( 'Bar' ),
+                       'Peek should return null if the service was never accessed.'
+               );
+       }
+
+       public function testPeekService_fail_unknown() {
+               $services = $this->newServiceContainer();
+
+               $name = 'TestService92834576';
+
+               $this->setExpectedException( Wikimedia\Services\NoSuchServiceException::class );
+
+               $services->peekService( $name );
+       }
+
+       public function testDefineService() {
+               $services = $this->newServiceContainer();
+
+               $theService = new stdClass();
+               $name = 'TestService92834576';
+
+               $services->defineService( $name, function ( $actualLocator ) use ( $services, $theService ) {
+                       PHPUnit_Framework_Assert::assertSame( $services, $actualLocator );
+                       return $theService;
+               } );
+
+               $this->assertTrue( $services->hasService( $name ) );
+               $this->assertSame( $theService, $services->getService( $name ) );
+       }
+
+       public function testDefineService_fail_duplicate() {
+               $services = $this->newServiceContainer();
+
+               $theService = new stdClass();
+               $name = 'TestService92834576';
+
+               $services->defineService( $name, function () use ( $theService ) {
+                       return $theService;
+               } );
+
+               $this->setExpectedException( Wikimedia\Services\ServiceAlreadyDefinedException::class );
+
+               $services->defineService( $name, function () use ( $theService ) {
+                       return $theService;
+               } );
+       }
+
+       public function testApplyWiring() {
+               $services = $this->newServiceContainer();
+
+               $wiring = [
+                       'Foo' => function () {
+                               return 'Foo!';
+                       },
+                       'Bar' => function () {
+                               return 'Bar!';
+                       },
+               ];
+
+               $services->applyWiring( $wiring );
+
+               $this->assertSame( 'Foo!', $services->getService( 'Foo' ) );
+               $this->assertSame( 'Bar!', $services->getService( 'Bar' ) );
+       }
+
+       public function testImportWiring() {
+               $services = $this->newServiceContainer();
+
+               $wiring = [
+                       'Foo' => function () {
+                               return 'Foo!';
+                       },
+                       'Bar' => function () {
+                               return 'Bar!';
+                       },
+                       'Car' => function () {
+                               return 'FUBAR!';
+                       },
+               ];
+
+               $services->applyWiring( $wiring );
+
+               $services->addServiceManipulator( 'Foo', function ( $service ) {
+                       return $service . '+X';
+               } );
+
+               $services->addServiceManipulator( 'Car', function ( $service ) {
+                       return $service . '+X';
+               } );
+
+               $newServices = $this->newServiceContainer();
+
+               // create a service with manipulator
+               $newServices->defineService( 'Foo', function () {
+                       return 'Foo!';
+               } );
+
+               $newServices->addServiceManipulator( 'Foo', function ( $service ) {
+                       return $service . '+Y';
+               } );
+
+               // create a service before importing, so we can later check that
+               // existing service instances survive importWiring()
+               $newServices->defineService( 'Car', function () {
+                       return 'Car!';
+               } );
+
+               // force instantiation
+               $newServices->getService( 'Car' );
+
+               // Define another service, so we can later check that extra wiring
+               // is not lost.
+               $newServices->defineService( 'Xar', function () {
+                       return 'Xar!';
+               } );
+
+               // import wiring, but skip `Bar`
+               $newServices->importWiring( $services, [ 'Bar' ] );
+
+               $this->assertNotContains( 'Bar', $newServices->getServiceNames(), 'Skip `Bar` service' );
+               $this->assertSame( 'Foo!+Y+X', $newServices->getService( 'Foo' ) );
+
+               // import all wiring, but preserve existing service instance
+               $newServices->importWiring( $services );
+
+               $this->assertContains( 'Bar', $newServices->getServiceNames(), 'Import all services' );
+               $this->assertSame( 'Bar!', $newServices->getService( 'Bar' ) );
+               $this->assertSame( 'Car!', $newServices->getService( 'Car' ), 'Use existing service instance' );
+               $this->assertSame( 'Xar!', $newServices->getService( 'Xar' ), 'Predefined services are kept' );
+       }
+
+       public function testLoadWiringFiles() {
+               $services = $this->newServiceContainer();
+
+               $wiringFiles = [
+                       __DIR__ . '/TestWiring1.php',
+                       __DIR__ . '/TestWiring2.php',
+               ];
+
+               $services->loadWiringFiles( $wiringFiles );
+
+               $this->assertSame( 'Foo!', $services->getService( 'Foo' ) );
+               $this->assertSame( 'Bar!', $services->getService( 'Bar' ) );
+       }
+
+       public function testLoadWiringFiles_fail_duplicate() {
+               $services = $this->newServiceContainer();
+
+               $wiringFiles = [
+                       __DIR__ . '/TestWiring1.php',
+                       __DIR__ . '/./TestWiring1.php',
+               ];
+
+               // loading the same file twice should fail, because
+               $this->setExpectedException( Wikimedia\Services\ServiceAlreadyDefinedException::class );
+
+               $services->loadWiringFiles( $wiringFiles );
+       }
+
+       public function testRedefineService() {
+               $services = $this->newServiceContainer( [ 'Foo' ] );
+
+               $theService1 = new stdClass();
+               $name = 'TestService92834576';
+
+               $services->defineService( $name, function () {
+                       PHPUnit_Framework_Assert::fail(
+                               'The original instantiator function should not get called'
+                       );
+               } );
+
+               // redefine before instantiation
+               $services->redefineService(
+                       $name,
+                       function ( $actualLocator, $extra ) use ( $services, $theService1 ) {
+                               PHPUnit_Framework_Assert::assertSame( $services, $actualLocator );
+                               PHPUnit_Framework_Assert::assertSame( 'Foo', $extra );
+                               return $theService1;
+                       }
+               );
+
+               // force instantiation, check result
+               $this->assertSame( $theService1, $services->getService( $name ) );
+       }
+
+       public function testRedefineService_disabled() {
+               $services = $this->newServiceContainer( [ 'Foo' ] );
+
+               $theService1 = new stdClass();
+               $name = 'TestService92834576';
+
+               $services->defineService( $name, function () {
+                       return 'Foo';
+               } );
+
+               // disable the service. we should be able to redefine it anyway.
+               $services->disableService( $name );
+
+               $services->redefineService( $name, function () use ( $theService1 ) {
+                       return $theService1;
+               } );
+
+               // force instantiation, check result
+               $this->assertSame( $theService1, $services->getService( $name ) );
+       }
+
+       public function testRedefineService_fail_undefined() {
+               $services = $this->newServiceContainer();
+
+               $theService = new stdClass();
+               $name = 'TestService92834576';
+
+               $this->setExpectedException( Wikimedia\Services\NoSuchServiceException::class );
+
+               $services->redefineService( $name, function () use ( $theService ) {
+                       return $theService;
+               } );
+       }
+
+       public function testRedefineService_fail_in_use() {
+               $services = $this->newServiceContainer( [ 'Foo' ] );
+
+               $theService = new stdClass();
+               $name = 'TestService92834576';
+
+               $services->defineService( $name, function () {
+                       return 'Foo';
+               } );
+
+               // create the service, so it can no longer be redefined
+               $services->getService( $name );
+
+               $this->setExpectedException( Wikimedia\Services\CannotReplaceActiveServiceException::class );
+
+               $services->redefineService( $name, function () use ( $theService ) {
+                       return $theService;
+               } );
+       }
+
+       public function testAddServiceManipulator() {
+               $services = $this->newServiceContainer( [ 'Foo' ] );
+
+               $theService1 = new stdClass();
+               $theService2 = new stdClass();
+               $name = 'TestService92834576';
+
+               $services->defineService(
+                       $name,
+                       function ( $actualLocator, $extra ) use ( $services, $theService1 ) {
+                               PHPUnit_Framework_Assert::assertSame( $services, $actualLocator );
+                               PHPUnit_Framework_Assert::assertSame( 'Foo', $extra );
+                               return $theService1;
+                       }
+               );
+
+               $services->addServiceManipulator(
+                       $name,
+                       function (
+                               $theService, $actualLocator, $extra
+                       ) use (
+                               $services, $theService1, $theService2
+                       ) {
+                               PHPUnit_Framework_Assert::assertSame( $theService1, $theService );
+                               PHPUnit_Framework_Assert::assertSame( $services, $actualLocator );
+                               PHPUnit_Framework_Assert::assertSame( 'Foo', $extra );
+                               return $theService2;
+                       }
+               );
+
+               // force instantiation, check result
+               $this->assertSame( $theService2, $services->getService( $name ) );
+       }
+
+       public function testAddServiceManipulator_fail_undefined() {
+               $services = $this->newServiceContainer();
+
+               $theService = new stdClass();
+               $name = 'TestService92834576';
+
+               $this->setExpectedException( Wikimedia\Services\NoSuchServiceException::class );
+
+               $services->addServiceManipulator( $name, function () use ( $theService ) {
+                       return $theService;
+               } );
+       }
+
+       public function testAddServiceManipulator_fail_in_use() {
+               $services = $this->newServiceContainer( [ 'Foo' ] );
+
+               $theService = new stdClass();
+               $name = 'TestService92834576';
+
+               $services->defineService( $name, function () use ( $theService ) {
+                       return $theService;
+               } );
+
+               // create the service, so it can no longer be redefined
+               $services->getService( $name );
+
+               $this->setExpectedException( Wikimedia\Services\CannotReplaceActiveServiceException::class );
+
+               $services->addServiceManipulator( $name, function () {
+                       return 'Foo';
+               } );
+       }
+
+       public function testDisableService() {
+               $services = $this->newServiceContainer( [ 'Foo' ] );
+
+               $destructible = $this->getMockBuilder( Wikimedia\Services\DestructibleService::class )
+                       ->getMock();
+               $destructible->expects( $this->once() )
+                       ->method( 'destroy' );
+
+               $services->defineService( 'Foo', function () use ( $destructible ) {
+                       return $destructible;
+               } );
+               $services->defineService( 'Bar', function () {
+                       return new stdClass();
+               } );
+               $services->defineService( 'Qux', function () {
+                       return new stdClass();
+               } );
+
+               // instantiate Foo and Bar services
+               $services->getService( 'Foo' );
+               $services->getService( 'Bar' );
+
+               // disable service, should call destroy() once.
+               $services->disableService( 'Foo' );
+
+               // disabled service should still be listed
+               $this->assertContains( 'Foo', $services->getServiceNames() );
+
+               // getting other services should still work
+               $services->getService( 'Bar' );
+
+               // disable non-destructible service, and not-yet-instantiated service
+               $services->disableService( 'Bar' );
+               $services->disableService( 'Qux' );
+
+               $this->assertNull( $services->peekService( 'Bar' ) );
+               $this->assertNull( $services->peekService( 'Qux' ) );
+
+               // disabled service should still be listed
+               $this->assertContains( 'Bar', $services->getServiceNames() );
+               $this->assertContains( 'Qux', $services->getServiceNames() );
+
+               $this->setExpectedException( Wikimedia\Services\ServiceDisabledException::class );
+               $services->getService( 'Qux' );
+       }
+
+       public function testDisableService_fail_undefined() {
+               $services = $this->newServiceContainer();
+
+               $theService = new stdClass();
+               $name = 'TestService92834576';
+
+               $this->setExpectedException( Wikimedia\Services\NoSuchServiceException::class );
+
+               $services->redefineService( $name, function () use ( $theService ) {
+                       return $theService;
+               } );
+       }
+
+       public function testDestroy() {
+               $services = $this->newServiceContainer();
+
+               $destructible = $this->getMockBuilder( Wikimedia\Services\DestructibleService::class )
+                       ->getMock();
+               $destructible->expects( $this->once() )
+                       ->method( 'destroy' );
+
+               $services->defineService( 'Foo', function () use ( $destructible ) {
+                       return $destructible;
+               } );
+
+               $services->defineService( 'Bar', function () {
+                       return new stdClass();
+               } );
+
+               // create the service
+               $services->getService( 'Foo' );
+
+               // destroy the container
+               $services->destroy();
+
+               $this->setExpectedException( Wikimedia\Services\ContainerDisabledException::class );
+               $services->getService( 'Bar' );
+       }
+
+}
diff --git a/tests/phpunit/unit/includes/libs/services/TestWiring1.php b/tests/phpunit/unit/includes/libs/services/TestWiring1.php
new file mode 100644 (file)
index 0000000..b6ff4eb
--- /dev/null
@@ -0,0 +1,10 @@
+<?php
+/**
+ * Test file for testing ServiceContainer::loadWiringFiles
+ */
+
+return [
+       'Foo' => function () {
+               return 'Foo!';
+       },
+];
diff --git a/tests/phpunit/unit/includes/libs/services/TestWiring2.php b/tests/phpunit/unit/includes/libs/services/TestWiring2.php
new file mode 100644 (file)
index 0000000..dfff64f
--- /dev/null
@@ -0,0 +1,10 @@
+<?php
+/**
+ * Test file for testing ServiceContainer::loadWiringFiles
+ */
+
+return [
+       'Bar' => function () {
+               return 'Bar!';
+       },
+];
diff --git a/tests/phpunit/unit/includes/libs/stats/PrefixingStatsdDataFactoryProxyTest.php b/tests/phpunit/unit/includes/libs/stats/PrefixingStatsdDataFactoryProxyTest.php
new file mode 100644 (file)
index 0000000..46e23e3
--- /dev/null
@@ -0,0 +1,58 @@
+<?php
+
+use Liuggio\StatsdClient\Factory\StatsdDataFactoryInterface;
+
+/**
+ * @covers PrefixingStatsdDataFactoryProxy
+ */
+class PrefixingStatsdDataFactoryProxyTest extends PHPUnit\Framework\TestCase {
+
+       use PHPUnit4And6Compat;
+
+       public function provideMethodNames() {
+               return [
+                       [ 'timing' ],
+                       [ 'gauge' ],
+                       [ 'set' ],
+                       [ 'increment' ],
+                       [ 'decrement' ],
+                       [ 'updateCount' ],
+                       [ 'produceStatsdData' ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideMethodNames
+        */
+       public function testPrefixingAndPassthrough( $method ) {
+               /** @var StatsdDataFactoryInterface|PHPUnit_Framework_MockObject_MockObject $innerFactory */
+               $innerFactory = $this->getMock(
+                       \Liuggio\StatsdClient\Factory\StatsdDataFactoryInterface::class
+               );
+               $innerFactory->expects( $this->once() )
+                       ->method( $method )
+                       ->with( 'testprefix.metricname' );
+
+               $proxy = new PrefixingStatsdDataFactoryProxy( $innerFactory, 'testprefix' );
+               // 1,2,3,4 simply makes sure we provide enough parameters, without caring what they are
+               $proxy->$method( 'metricname', 1, 2, 3, 4 );
+       }
+
+       /**
+        * @dataProvider provideMethodNames
+        */
+       public function testPrefixIsTrimmed( $method ) {
+               /** @var StatsdDataFactoryInterface|PHPUnit_Framework_MockObject_MockObject $innerFactory */
+               $innerFactory = $this->getMock(
+                       \Liuggio\StatsdClient\Factory\StatsdDataFactoryInterface::class
+               );
+               $innerFactory->expects( $this->once() )
+                       ->method( $method )
+                       ->with( 'testprefix.metricname' );
+
+               $proxy = new PrefixingStatsdDataFactoryProxy( $innerFactory, 'testprefix...' );
+               // 1,2,3,4 simply makes sure we provide enough parameters, without caring what they are
+               $proxy->$method( 'metricname', 1, 2, 3, 4 );
+       }
+
+}
diff --git a/tests/phpunit/unit/includes/media/GIFMetadataExtractorTest.php b/tests/phpunit/unit/includes/media/GIFMetadataExtractorTest.php
new file mode 100644 (file)
index 0000000..10c450d
--- /dev/null
@@ -0,0 +1,110 @@
+<?php
+
+/**
+ * @group Media
+ */
+class GIFMetadataExtractorTest extends \MediaWikiUnitTestCase {
+
+       protected function setUp() {
+               parent::setUp();
+
+               $this->mediaPath = __DIR__ . '/../../../data/media/';
+       }
+
+       /**
+        * Put in a file, and see if the metadata coming out is as expected.
+        * @param string $filename
+        * @param array $expected The extracted metadata.
+        * @dataProvider provideGetMetadata
+        * @covers GIFMetadataExtractor::getMetadata
+        */
+       public function testGetMetadata( $filename, $expected ) {
+               $actual = GIFMetadataExtractor::getMetadata( $this->mediaPath . $filename );
+               $this->assertEquals( $expected, $actual );
+       }
+
+       public static function provideGetMetadata() {
+               $xmpNugget = <<<EOF
+<?xpacket begin='' id='W5M0MpCehiHzreSzNTczkc9d'?>
+<x:xmpmeta xmlns:x='adobe:ns:meta/' x:xmptk='Image::ExifTool 7.30'>
+<rdf:RDF xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#'>
+
+ <rdf:Description rdf:about=''
+  xmlns:Iptc4xmpCore='http://iptc.org/std/Iptc4xmpCore/1.0/xmlns/'>
+  <Iptc4xmpCore:Location>The interwebs</Iptc4xmpCore:Location>
+ </rdf:Description>
+
+ <rdf:Description rdf:about=''
+  xmlns:tiff='http://ns.adobe.com/tiff/1.0/'>
+  <tiff:Artist>Bawolff</tiff:Artist>
+  <tiff:ImageDescription>
+   <rdf:Alt>
+    <rdf:li xml:lang='x-default'>A file to test GIF</rdf:li>
+   </rdf:Alt>
+  </tiff:ImageDescription>
+ </rdf:Description>
+</rdf:RDF>
+</x:xmpmeta>
+                                                                                                    
+                                                                                                    
+                                                                                                    
+                                                                                                    
+                                                                                                    
+                                                                                                    
+                                                                                                    
+                                                                                                    
+                                                                                                    
+                                                                                                    
+                                                                                                    
+                                                                                                    
+                                                                                                    
+                                                                                                    
+                                                                                                    
+                                                                                                    
+                                                                                                    
+                                                                                                    
+                                                                                                    
+                                                                                                    
+                                                                                                    
+                                                                                                    
+                                                                                                    
+                                                                                                    
+<?xpacket end='w'?>
+EOF;
+               $xmpNugget = str_replace( "\r", '', $xmpNugget ); // Windows compat
+
+               return [
+                       [
+                               'nonanimated.gif',
+                               [
+                                       'comment' => [ 'GIF test file ⁕ Created with GIMP' ],
+                                       'duration' => 0.1,
+                                       'frameCount' => 1,
+                                       'looped' => false,
+                                       'xmp' => '',
+                               ]
+                       ],
+                       [
+                               'animated.gif',
+                               [
+                                       'comment' => [ 'GIF test file . Created with GIMP' ],
+                                       'duration' => 2.4,
+                                       'frameCount' => 4,
+                                       'looped' => true,
+                                       'xmp' => '',
+                               ]
+                       ],
+
+                       [
+                               'animated-xmp.gif',
+                               [
+                                       'xmp' => $xmpNugget,
+                                       'duration' => 2.4,
+                                       'frameCount' => 4,
+                                       'looped' => true,
+                                       'comment' => [ 'GIƒ·test·file' ],
+                               ]
+                       ],
+               ];
+       }
+}
diff --git a/tests/phpunit/unit/includes/media/IPTCTest.php b/tests/phpunit/unit/includes/media/IPTCTest.php
new file mode 100644 (file)
index 0000000..430493c
--- /dev/null
@@ -0,0 +1,85 @@
+<?php
+
+/**
+ * @group Media
+ */
+class IPTCTest extends \MediaWikiUnitTestCase {
+
+       /**
+        * @covers IPTC::getCharset
+        */
+       public function testRecognizeUtf8() {
+               // utf-8 is the only one used in practise.
+               $res = IPTC::getCharset( "\x1b%G" );
+               $this->assertEquals( 'UTF-8', $res );
+       }
+
+       /**
+        * @covers IPTC::parse
+        */
+       public function testIPTCParseNoCharset88591() {
+               // basically IPTC for keyword with value of 0xBC which is 1/4 in iso-8859-1
+               // This data doesn't specify a charset. We're supposed to guess
+               // (which basically means utf-8 if valid, windows 1252 (iso 8859-1) if not)
+               $iptcData = "Photoshop 3.0\08BIM\4\4\0\0\0\0\0\x06\x1c\x02\x19\x00\x01\xBC";
+               $res = IPTC::parse( $iptcData );
+               $this->assertEquals( [ '¼' ], $res['Keywords'] );
+       }
+
+       /**
+        * @covers IPTC::parse
+        */
+       public function testIPTCParseNoCharset88591b() {
+               /* This one contains a sequence that's valid iso 8859-1 but not valid utf8 */
+               /* \xC3 = Ã, \xB8 = ¸  */
+               $iptcData = "Photoshop 3.0\08BIM\4\4\0\0\0\0\0\x09\x1c\x02\x19\x00\x04\xC3\xC3\xC3\xB8";
+               $res = IPTC::parse( $iptcData );
+               $this->assertEquals( [ 'ÃÃø' ], $res['Keywords'] );
+       }
+
+       /**
+        * Same as testIPTCParseNoCharset88591b, but forcing the charset to utf-8.
+        * What should happen is the first "\xC3\xC3" should be dropped as invalid,
+        * leaving \xC3\xB8, which is ø
+        * @covers IPTC::parse
+        */
+       public function testIPTCParseForcedUTFButInvalid() {
+               $iptcData = "Photoshop 3.0\08BIM\4\4\0\0\0\0\0\x11\x1c\x02\x19\x00\x04\xC3\xC3\xC3\xB8"
+                       . "\x1c\x01\x5A\x00\x03\x1B\x25\x47";
+               $res = IPTC::parse( $iptcData );
+               $this->assertEquals( [ 'ø' ], $res['Keywords'] );
+       }
+
+       /**
+        * @covers IPTC::parse
+        */
+       public function testIPTCParseNoCharsetUTF8() {
+               $iptcData = "Photoshop 3.0\08BIM\4\4\0\0\0\0\0\x07\x1c\x02\x19\x00\x02¼";
+               $res = IPTC::parse( $iptcData );
+               $this->assertEquals( [ '¼' ], $res['Keywords'] );
+       }
+
+       /**
+        * Testing something that has 2 values for keyword
+        * @covers IPTC::parse
+        */
+       public function testIPTCParseMulti() {
+               $iptcData = /* identifier */ "Photoshop 3.0\08BIM\4\4"
+                       /* length */ . "\0\0\0\0\0\x0D"
+                       . "\x1c\x02\x19" . "\x00\x01" . "\xBC"
+                       . "\x1c\x02\x19" . "\x00\x02" . "\xBC\xBD";
+               $res = IPTC::parse( $iptcData );
+               $this->assertEquals( [ '¼', '¼½' ], $res['Keywords'] );
+       }
+
+       /**
+        * @covers IPTC::parse
+        */
+       public function testIPTCParseUTF8() {
+               // This has the magic "\x1c\x01\x5A\x00\x03\x1B\x25\x47" which marks content as UTF8.
+               $iptcData =
+                       "Photoshop 3.0\08BIM\4\4\0\0\0\0\0\x0F\x1c\x02\x19\x00\x02¼\x1c\x01\x5A\x00\x03\x1B\x25\x47";
+               $res = IPTC::parse( $iptcData );
+               $this->assertEquals( [ '¼' ], $res['Keywords'] );
+       }
+}
diff --git a/tests/phpunit/unit/includes/media/JpegMetadataExtractorTest.php b/tests/phpunit/unit/includes/media/JpegMetadataExtractorTest.php
new file mode 100644 (file)
index 0000000..6063f3e
--- /dev/null
@@ -0,0 +1,128 @@
+<?php
+/**
+ * @todo Could use a test of extended XMP segments. Hard to find programs that
+ * create example files, and creating my own in vim propbably wouldn't
+ * serve as a very good "test". (Adobe photoshop probably creates such files
+ * but it costs money). The implementation of it currently in MediaWiki is based
+ * solely on reading the standard, without any real world test files.
+ *
+ * @group Media
+ * @covers JpegMetadataExtractor
+ */
+class JpegMetadataExtractorTest extends \MediaWikiUnitTestCase {
+
+       protected $filePath;
+
+       protected function setUp() {
+               parent::setUp();
+
+               $this->filePath = __DIR__ . '/../../../data/media/';
+       }
+
+       /**
+        * We also use this test to test padding bytes don't
+        * screw stuff up
+        *
+        * @param string $file Filename
+        *
+        * @dataProvider provideUtf8Comment
+        */
+       public function testUtf8Comment( $file ) {
+               $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . $file );
+               $this->assertEquals( [ 'UTF-8 JPEG Comment — ¼' ], $res['COM'] );
+       }
+
+       public static function provideUtf8Comment() {
+               return [
+                       [ 'jpeg-comment-utf.jpg' ],
+                       [ 'jpeg-padding-even.jpg' ],
+                       [ 'jpeg-padding-odd.jpg' ],
+               ];
+       }
+
+       /** The file is iso-8859-1, but it should get auto converted */
+       public function testIso88591Comment() {
+               $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-comment-iso8859-1.jpg' );
+               $this->assertEquals( [ 'ISO-8859-1 JPEG Comment - ¼' ], $res['COM'] );
+       }
+
+       /** Comment values that are non-textual (random binary junk) should not be shown.
+        * The example test file has a comment with a 0x5 byte in it which is a control character
+        * and considered binary junk for our purposes.
+        */
+       public function testBinaryCommentStripped() {
+               $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-comment-binary.jpg' );
+               $this->assertEmpty( $res['COM'] );
+       }
+
+       /* Very rarely a file can have multiple comments.
+        *   Order of comments is based on order inside the file.
+        */
+       public function testMultipleComment() {
+               $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-comment-multiple.jpg' );
+               $this->assertEquals( [ 'foo', 'bar' ], $res['COM'] );
+       }
+
+       public function testXMPExtraction() {
+               $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-xmp-psir.jpg' );
+               $expected = file_get_contents( $this->filePath . 'jpeg-xmp-psir.xmp' );
+               $this->assertEquals( $expected, $res['XMP'] );
+       }
+
+       public function testPSIRExtraction() {
+               $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-xmp-psir.jpg' );
+               $expected = '50686f746f73686f7020332e30003842494d04040000000'
+                       . '000181c02190004746573741c02190003666f6f1c020000020004';
+               $this->assertEquals( $expected, bin2hex( $res['PSIR'][0] ) );
+       }
+
+       public function testXMPExtractionAltAppId() {
+               $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-xmp-alt.jpg' );
+               $expected = file_get_contents( $this->filePath . 'jpeg-xmp-psir.xmp' );
+               $this->assertEquals( $expected, $res['XMP'] );
+       }
+
+       public function testIPTCHashComparisionNoHash() {
+               $segments = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-xmp-psir.jpg' );
+               $res = JpegMetadataExtractor::doPSIR( $segments['PSIR'][0] );
+
+               $this->assertEquals( 'iptc-no-hash', $res );
+       }
+
+       public function testIPTCHashComparisionBadHash() {
+               $segments = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-iptc-bad-hash.jpg' );
+               $res = JpegMetadataExtractor::doPSIR( $segments['PSIR'][0] );
+
+               $this->assertEquals( 'iptc-bad-hash', $res );
+       }
+
+       public function testIPTCHashComparisionGoodHash() {
+               $segments = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-iptc-good-hash.jpg' );
+               $res = JpegMetadataExtractor::doPSIR( $segments['PSIR'][0] );
+
+               $this->assertEquals( 'iptc-good-hash', $res );
+       }
+
+       public function testExifByteOrder() {
+               $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'exif-user-comment.jpg' );
+               $expected = 'BE';
+               $this->assertEquals( $expected, $res['byteOrder'] );
+       }
+
+       public function testInfiniteRead() {
+               // test file truncated right after a segment, which previously
+               // caused an infinite loop looking for the next segment byte.
+               // Should get past infinite loop and throw in wfUnpack()
+               $this->setExpectedException( 'MWException' );
+               $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-segment-loop1.jpg' );
+       }
+
+       public function testInfiniteRead2() {
+               // test file truncated after a segment's marker and size, which
+               // would cause a seek past end of file. Seek past end of file
+               // doesn't actually fail, but prevents further reading and was
+               // devolving into the previous case (testInfiniteRead).
+               $this->setExpectedException( 'MWException' );
+               $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-segment-loop2.jpg' );
+       }
+}
diff --git a/tests/phpunit/unit/includes/media/MediaHandlerTest.php b/tests/phpunit/unit/includes/media/MediaHandlerTest.php
new file mode 100644 (file)
index 0000000..eb4ece8
--- /dev/null
@@ -0,0 +1,68 @@
+<?php
+
+/**
+ * @group Media
+ */
+class MediaHandlerTest extends \MediaWikiUnitTestCase {
+
+       /**
+        * @covers MediaHandler::fitBoxWidth
+        *
+        * @dataProvider provideTestFitBoxWidth
+        */
+       public function testFitBoxWidth( $width, $height, $max, $expected ) {
+               $y = round( $expected * $height / $width );
+               $result = MediaHandler::fitBoxWidth( $width, $height, $max );
+               $y2 = round( $result * $height / $width );
+               $this->assertEquals( $expected,
+                       $result,
+                       "($width, $height, $max) wanted: {$expected}x$y, got: {z$result}x$y2" );
+       }
+
+       public static function provideTestFitBoxWidth() {
+               return array_merge(
+                       static::generateTestFitBoxWidthData( 50, 50, [
+                                       50 => 50,
+                                       17 => 17,
+                                       18 => 18 ]
+                       ),
+                       static::generateTestFitBoxWidthData( 366, 300, [
+                                       50 => 61,
+                                       17 => 21,
+                                       18 => 22 ]
+                       ),
+                       static::generateTestFitBoxWidthData( 300, 366, [
+                                       50 => 41,
+                                       17 => 14,
+                                       18 => 15 ]
+                       ),
+                       static::generateTestFitBoxWidthData( 100, 400, [
+                                       50 => 12,
+                                       17 => 4,
+                                       18 => 4 ]
+                       )
+               );
+       }
+
+       /**
+        * Generate single test cases by combining the dimensions and tests contents
+        *
+        * It creates:
+        * [$width, $height, $max, $expected],
+        * [$width, $height, $max2, $expected2], ...
+        * out of parameters:
+        * $width, $height, { $max => $expected, $max2 => $expected2, ... }
+        *
+        * @param int $width
+        * @param int $height
+        * @param array $tests associative array of $max => $expected values
+        * @return array
+        */
+       private static function generateTestFitBoxWidthData( $width, $height, $tests ) {
+               $result = [];
+               foreach ( $tests as $max => $expected ) {
+                       $result[] = [ $width, $height, $max, $expected ];
+               }
+               return $result;
+       }
+}
diff --git a/tests/phpunit/unit/includes/media/SVGMetadataExtractorTest.php b/tests/phpunit/unit/includes/media/SVGMetadataExtractorTest.php
new file mode 100644 (file)
index 0000000..30d1008
--- /dev/null
@@ -0,0 +1,201 @@
+<?php
+
+/**
+ * @group Media
+ * @covers SVGMetadataExtractor
+ */
+class SVGMetadataExtractorTest extends \MediaWikiUnitTestCase {
+
+       /**
+        * @dataProvider provideSvgFiles
+        */
+       public function testGetMetadata( $infile, $expected ) {
+               $this->assertMetadata( $infile, $expected );
+       }
+
+       /**
+        * @dataProvider provideSvgFilesWithXMLMetadata
+        */
+       public function testGetXMLMetadata( $infile, $expected ) {
+               $r = new XMLReader();
+               $this->assertMetadata( $infile, $expected );
+       }
+
+       /**
+        * @dataProvider provideSvgUnits
+        */
+       public function testScaleSVGUnit( $inUnit, $expected ) {
+               $this->assertEquals(
+                       $expected,
+                       SVGReader::scaleSVGUnit( $inUnit ),
+                       'SVG unit conversion and scaling failure'
+               );
+       }
+
+       function assertMetadata( $infile, $expected ) {
+               try {
+                       $data = SVGMetadataExtractor::getMetadata( $infile );
+                       $this->assertEquals( $expected, $data, 'SVG metadata extraction test' );
+               } catch ( MWException $e ) {
+                       if ( $expected === false ) {
+                               $this->assertTrue( true, 'SVG metadata extracted test (expected failure)' );
+                       } else {
+                               throw $e;
+                       }
+               }
+       }
+
+       public static function provideSvgFiles() {
+               $base = __DIR__ . '/../../../data/media';
+
+               return [
+                       [
+                               "$base/Wikimedia-logo.svg",
+                               [
+                                       'width' => 1024,
+                                       'height' => 1024,
+                                       'originalWidth' => '1024',
+                                       'originalHeight' => '1024',
+                                       'translations' => [],
+                               ]
+                       ],
+                       [
+                               "$base/QA_icon.svg",
+                               [
+                                       'width' => 60,
+                                       'height' => 60,
+                                       'originalWidth' => '60',
+                                       'originalHeight' => '60',
+                                       'translations' => [],
+                               ]
+                       ],
+                       [
+                               "$base/Gtk-media-play-ltr.svg",
+                               [
+                                       'width' => 60,
+                                       'height' => 60,
+                                       'originalWidth' => '60.0000000',
+                                       'originalHeight' => '60.0000000',
+                                       'translations' => [],
+                               ]
+                       ],
+                       [
+                               "$base/Toll_Texas_1.svg",
+                               // This file triggered T33719, needs entity expansion in the xmlns checks
+                               [
+                                       'width' => 385,
+                                       'height' => 385,
+                                       'originalWidth' => '385',
+                                       'originalHeight' => '385.0004883',
+                                       'translations' => [],
+                               ]
+                       ],
+                       [
+                               "$base/Tux.svg",
+                               [
+                                       'width' => 512,
+                                       'height' => 594,
+                                       'originalWidth' => '100%',
+                                       'originalHeight' => '100%',
+                                       'title' => 'Tux',
+                                       'translations' => [],
+                                       'description' => 'For more information see: http://commons.wikimedia.org/wiki/Image:Tux.svg',
+                               ]
+                       ],
+                       [
+                               "$base/Speech_bubbles.svg",
+                               [
+                                       'width' => 627,
+                                       'height' => 461,
+                                       'originalWidth' => '17.7cm',
+                                       'originalHeight' => '13cm',
+                                       'translations' => [
+                                               'de' => SVGReader::LANG_FULL_MATCH,
+                                               'fr' => SVGReader::LANG_FULL_MATCH,
+                                               'nl' => SVGReader::LANG_FULL_MATCH,
+                                               'tlh-ca' => SVGReader::LANG_FULL_MATCH,
+                                               'tlh' => SVGReader::LANG_PREFIX_MATCH
+                                       ],
+                               ]
+                       ],
+                       [
+                               "$base/Soccer_ball_animated.svg",
+                               [
+                                       'width' => 150,
+                                       'height' => 150,
+                                       'originalWidth' => '150',
+                                       'originalHeight' => '150',
+                                       'animated' => true,
+                                       'translations' => []
+                               ],
+                       ],
+                       [
+                               "$base/comma_separated_viewbox.svg",
+                               [
+                                       'width' => 512,
+                                       'height' => 594,
+                                       'originalWidth' => '100%',
+                                       'originalHeight' => '100%',
+                                       'translations' => []
+                               ],
+                       ],
+               ];
+       }
+
+       public static function provideSvgFilesWithXMLMetadata() {
+               $base = __DIR__ . '/../../../data/media';
+               // phpcs:disable Generic.Files.LineLength
+               $metadata = '<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
+      <ns4:Work xmlns:ns4="http://creativecommons.org/ns#" rdf:about="">
+        <ns5:format xmlns:ns5="http://purl.org/dc/elements/1.1/">image/svg+xml</ns5:format>
+        <ns5:type xmlns:ns5="http://purl.org/dc/elements/1.1/" rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
+      </ns4:Work>
+    </rdf:RDF>';
+               // phpcs:enable
+
+               $metadata = str_replace( "\r", '', $metadata ); // Windows compat
+               return [
+                       [
+                               "$base/US_states_by_total_state_tax_revenue.svg",
+                               [
+                                       'height' => 593,
+                                       'metadata' => $metadata,
+                                       'width' => 959,
+                                       'originalWidth' => '958.69',
+                                       'originalHeight' => '592.78998',
+                                       'translations' => [],
+                               ]
+                       ],
+               ];
+       }
+
+       public static function provideSvgUnits() {
+               return [
+                       [ '1' , 1 ],
+                       [ '1.1' , 1.1 ],
+                       [ '0.1' , 0.1 ],
+                       [ '.1' , 0.1 ],
+                       [ '1e2' , 100 ],
+                       [ '1E2' , 100 ],
+                       [ '+1' , 1 ],
+                       [ '-1' , -1 ],
+                       [ '-1.1' , -1.1 ],
+                       [ '1e+2' , 100 ],
+                       [ '1e-2' , 0.01 ],
+                       [ '10px' , 10 ],
+                       [ '10pt' , 10 * 1.25 ],
+                       [ '10pc' , 10 * 15 ],
+                       [ '10mm' , 10 * 3.543307 ],
+                       [ '10cm' , 10 * 35.43307 ],
+                       [ '10in' , 10 * 90 ],
+                       [ '10em' , 10 * 16 ],
+                       [ '10ex' , 10 * 12 ],
+                       [ '10%' , 51.2 ],
+                       [ '10 px' , 10 ],
+                       // Invalid values
+                       [ '1e1.1', 10 ],
+                       [ '10bp', 10 ],
+                       [ 'p10', null ],
+               ];
+       }
+}
diff --git a/tests/phpunit/unit/includes/media/WebPHandlerTest.php b/tests/phpunit/unit/includes/media/WebPHandlerTest.php
new file mode 100644 (file)
index 0000000..6c8600d
--- /dev/null
@@ -0,0 +1,151 @@
+<?php
+
+/**
+ * @covers WebPHandler
+ */
+class WebPHandlerTest extends \MediaWikiUnitTestCase {
+       public function setUp() {
+               parent::setUp();
+               // Allocated file for testing
+               $this->tempFileName = tempnam( wfTempDir(), 'WEBP' );
+       }
+
+       public function tearDown() {
+               parent::tearDown();
+               unlink( $this->tempFileName );
+       }
+
+       /**
+        * @dataProvider provideTestExtractMetaData
+        */
+       public function testExtractMetaData( $header, $expectedResult ) {
+               // Put header into file
+               file_put_contents( $this->tempFileName, $header );
+
+               $this->assertEquals( $expectedResult, WebPHandler::extractMetadata( $this->tempFileName ) );
+       }
+
+       public function provideTestExtractMetaData() {
+               // phpcs:disable Generic.Files.LineLength
+               return [
+                       // Files from https://developers.google.com/speed/webp/gallery2
+                       [ "\x52\x49\x46\x46\x90\x68\x01\x00\x57\x45\x42\x50\x56\x50\x38\x4C\x83\x68\x01\x00\x2F\x8F\x01\x4B\x10\x8D\x38\x6C\xDB\x46\x92\xE0\xE0\x82\x7B\x6C",
+                               [ 'compression' => 'lossless', 'width' => 400, 'height' => 301 ] ],
+                       [ "\x52\x49\x46\x46\x64\x5B\x00\x00\x57\x45\x42\x50\x56\x50\x38\x58\x0A\x00\x00\x00\x10\x00\x00\x00\x8F\x01\x00\x2C\x01\x00\x41\x4C\x50\x48\xE5\x0E",
+                               [ 'compression' => 'unknown', 'animated' => false, 'transparency' => true, 'width' => 400, 'height' => 301 ] ],
+                       [ "\x52\x49\x46\x46\xA8\x72\x00\x00\x57\x45\x42\x50\x56\x50\x38\x4C\x9B\x72\x00\x00\x2F\x81\x81\x62\x10\x8D\x40\x8C\x24\x39\x6E\x73\x73\x38\x01\x96",
+                               [ 'compression' => 'lossless', 'width' => 386, 'height' => 395 ] ],
+                       [ "\x52\x49\x46\x46\xE0\x42\x00\x00\x57\x45\x42\x50\x56\x50\x38\x58\x0A\x00\x00\x00\x10\x00\x00\x00\x81\x01\x00\x8A\x01\x00\x41\x4C\x50\x48\x56\x10",
+                               [ 'compression' => 'unknown', 'animated' => false, 'transparency' => true, 'width' => 386, 'height' => 395 ] ],
+                       [ "\x52\x49\x46\x46\x70\x61\x02\x00\x57\x45\x42\x50\x56\x50\x38\x4C\x63\x61\x02\x00\x2F\x1F\xC3\x95\x10\x8D\xC8\x72\xDB\xC8\x92\x24\xD8\x91\xD9\x91",
+                               [ 'compression' => 'lossless', 'width' => 800, 'height' => 600 ] ],
+                       [ "\x52\x49\x46\x46\x1C\x1D\x01\x00\x57\x45\x42\x50\x56\x50\x38\x58\x0A\x00\x00\x00\x10\x00\x00\x00\x1F\x03\x00\x57\x02\x00\x41\x4C\x50\x48\x25\x8B",
+                               [ 'compression' => 'unknown', 'animated' => false, 'transparency' => true, 'width' => 800, 'height' => 600 ] ],
+                       [ "\x52\x49\x46\x46\xFA\xC5\x00\x00\x57\x45\x42\x50\x56\x50\x38\x4C\xEE\xC5\x00\x00\x2F\xA4\x81\x28\x10\x8D\x40\x68\x24\xC9\x91\xA4\xAE\xF3\x97\x75",
+                               [ 'compression' => 'lossless', 'width' => 421, 'height' => 163 ] ],
+                       [ "\x52\x49\x46\x46\xF6\x5D\x00\x00\x57\x45\x42\x50\x56\x50\x38\x58\x0A\x00\x00\x00\x10\x00\x00\x00\xA4\x01\x00\xA2\x00\x00\x41\x4C\x50\x48\x38\x1A",
+                               [ 'compression' => 'unknown', 'animated' => false, 'transparency' => true, 'width' => 421, 'height' => 163 ] ],
+                       [ "\x52\x49\x46\x46\xC4\x96\x01\x00\x57\x45\x42\x50\x56\x50\x38\x4C\xB8\x96\x01\x00\x2F\x2B\xC1\x4A\x10\x11\x87\x6D\xDB\x48\x12\xFC\x60\xB0\x83\x24",
+                               [ 'compression' => 'lossless', 'width' => 300, 'height' => 300 ] ],
+                       [ "\x52\x49\x46\x46\x0A\x11\x01\x00\x57\x45\x42\x50\x56\x50\x38\x58\x0A\x00\x00\x00\x10\x00\x00\x00\x2B\x01\x00\x2B\x01\x00\x41\x4C\x50\x48\x67\x6E",
+                               [ 'compression' => 'unknown', 'animated' => false, 'transparency' => true, 'width' => 300, 'height' => 300 ] ],
+
+                       // Lossy files from https://developers.google.com/speed/webp/gallery1
+                       [ "\x52\x49\x46\x46\x68\x76\x00\x00\x57\x45\x42\x50\x56\x50\x38\x20\x5C\x76\x00\x00\xD2\xBE\x01\x9D\x01\x2A\x26\x02\x70\x01\x3E\xD5\x4E\x97\x43\xA2",
+                               [ 'compression' => 'lossy', 'width' => 550, 'height' => 368 ] ],
+                       [ "\x52\x49\x46\x46\xB0\xEC\x00\x00\x57\x45\x42\x50\x56\x50\x38\x20\xA4\xEC\x00\x00\xB2\x4B\x02\x9D\x01\x2A\x26\x02\x94\x01\x3E\xD1\x50\x96\x46\x26",
+                               [ 'compression' => 'lossy', 'width' => 550, 'height' => 404 ] ],
+                       [ "\x52\x49\x46\x46\x7A\x19\x03\x00\x57\x45\x42\x50\x56\x50\x38\x20\x6E\x19\x03\x00\xB2\xF8\x09\x9D\x01\x2A\x00\x05\xD0\x02\x3E\xAD\x46\x99\x4A\xA5",
+                               [ 'compression' => 'lossy', 'width' => 1280, 'height' => 720 ] ],
+                       [ "\x52\x49\x46\x46\x44\xB3\x02\x00\x57\x45\x42\x50\x56\x50\x38\x20\x38\xB3\x02\x00\x52\x57\x06\x9D\x01\x2A\x00\x04\x04\x03\x3E\xA5\x44\x96\x49\x26",
+                               [ 'compression' => 'lossy', 'width' => 1024, 'height' => 772 ] ],
+                       [ "\x52\x49\x46\x46\x02\x43\x01\x00\x57\x45\x42\x50\x56\x50\x38\x20\xF6\x42\x01\x00\x12\xC0\x05\x9D\x01\x2A\x00\x04\xF0\x02\x3E\x79\x34\x93\x47\xA4",
+                               [ 'compression' => 'lossy', 'width' => 1024, 'height' => 752 ] ],
+
+                       // Animated file from https://groups.google.com/a/chromium.org/d/topic/blink-dev/Y8tRC4mdQz8/discussion
+                       [ "\x52\x49\x46\x46\xD0\x0B\x02\x00\x57\x45\x42\x50\x56\x50\x38\x58\x0A\x00\x00\x00\x12\x00\x00\x00\x3F\x01\x00\x3F\x01\x00\x41\x4E",
+                               [ 'compression' => 'unknown', 'animated' => true, 'transparency' => true, 'width' => 320, 'height' => 320 ] ],
+
+                       // Error cases
+                       [ '', false ],
+                       [ '                                    ', false ],
+                       [ 'RIFF                                ', false ],
+                       [ 'RIFF1234WEBP                        ', false ],
+                       [ 'RIFF1234WEBPVP8                     ', false ],
+                       [ 'RIFF1234WEBPVP8L                    ', false ],
+               ];
+               // phpcs:enable
+       }
+
+       /**
+        * @dataProvider provideTestWithFileExtractMetaData
+        */
+       public function testWithFileExtractMetaData( $filename, $expectedResult ) {
+               $this->assertEquals( $expectedResult, WebPHandler::extractMetadata( $filename ) );
+       }
+
+       public function provideTestWithFileExtractMetaData() {
+               return [
+                       [ __DIR__ . '/../../../data/media/2_webp_ll.webp',
+                               [
+                                       'compression' => 'lossless',
+                                       'width' => 386,
+                                       'height' => 395
+                               ]
+                       ],
+                       [ __DIR__ . '/../../../data/media/2_webp_a.webp',
+                               [
+                                       'compression' => 'lossy',
+                                       'animated' => false,
+                                       'transparency' => true,
+                                       'width' => 386,
+                                       'height' => 395
+                               ]
+                       ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideTestGetImageSize
+        */
+       public function testGetImageSize( $path, $expectedResult ) {
+               $handler = new WebPHandler();
+               $this->assertEquals( $expectedResult, $handler->getImageSize( null, $path ) );
+       }
+
+       public function provideTestGetImageSize() {
+               return [
+                       // Public domain files from https://developers.google.com/speed/webp/gallery2
+                       [ __DIR__ . '/../../../data/media/2_webp_a.webp', [ 386, 395 ] ],
+                       [ __DIR__ . '/../../../data/media/2_webp_ll.webp', [ 386, 395 ] ],
+                       [ __DIR__ . '/../../../data/media/webp_animated.webp', [ 300, 225 ] ],
+
+                       // Error cases
+                       [ __FILE__, false ],
+               ];
+       }
+
+       /**
+        * Tests the WebP MIME detection. This should really be a separate test, but sticking it
+        * here for now.
+        *
+        * @dataProvider provideTestGetMimeType
+        */
+       public function testGuessMimeType( $path ) {
+               $mime = MediaWiki\MediaWikiServices::getInstance()->getMimeAnalyzer();
+               $this->assertEquals( 'image/webp', $mime->guessMimeType( $path, false ) );
+       }
+
+       public function provideTestGetMimeType() {
+               return [
+                               // Public domain files from https://developers.google.com/speed/webp/gallery2
+                               [ __DIR__ . '/../../../data/media/2_webp_a.webp' ],
+                               [ __DIR__ . '/../../../data/media/2_webp_ll.webp' ],
+                               [ __DIR__ . '/../../../data/media/webp_animated.webp' ],
+               ];
+       }
+}
+
+/* Python code to extract a header and convert to PHP format:
+ * print '"%s"' % ''.implode( '\\x%02X' % ord(c) for c in urllib.urlopen(url).read(36) )
+ */
diff --git a/tests/phpunit/unit/includes/objectcache/MemcachedBagOStuffTest.php b/tests/phpunit/unit/includes/objectcache/MemcachedBagOStuffTest.php
new file mode 100644 (file)
index 0000000..eb040b4
--- /dev/null
@@ -0,0 +1,107 @@
+<?php
+/**
+ * @group BagOStuff
+ */
+class MemcachedBagOStuffTest extends \MediaWikiUnitTestCase {
+       /** @var MemcachedBagOStuff */
+       private $cache;
+
+       protected function setUp() {
+               parent::setUp();
+               $this->cache = new MemcachedPhpBagOStuff( [ 'keyspace' => 'test', 'servers' => [] ] );
+       }
+
+       /**
+        * @covers MemcachedBagOStuff::makeKey
+        */
+       public function testKeyNormalization() {
+               $this->assertEquals(
+                       'test:vanilla',
+                       $this->cache->makeKey( 'vanilla' )
+               );
+
+               $this->assertEquals(
+                       'test:punctuation_marks_are_ok:!@$^&*()',
+                       $this->cache->makeKey( 'punctuation_marks_are_ok', '!@$^&*()' )
+               );
+
+               $this->assertEquals(
+                       'test:but_spaces:hashes%23:and%0Anewlines:are_not',
+                       $this->cache->makeKey( 'but spaces', 'hashes#', "and\nnewlines", 'are_not' )
+               );
+
+               $this->assertEquals(
+                       'test:this:key:contains:%F0%9D%95%9E%F0%9D%95%A6%F0%9D%95%9D%F0%9D%95%A5%F0%9' .
+                               'D%95%9A%F0%9D%95%93%F0%9D%95%AA%F0%9D%95%A5%F0%9D%95%96:characters',
+                       $this->cache->makeKey( 'this', 'key', 'contains', '𝕞𝕦𝕝𝕥𝕚𝕓𝕪𝕥𝕖', 'characters' )
+               );
+
+               $this->assertEquals(
+                       'test:this:key:contains:#c118f92685a635cb843039de50014c9c',
+                       $this->cache->makeKey( 'this', 'key', 'contains', '𝕥𝕠𝕠 𝕞𝕒𝕟𝕪 𝕞𝕦𝕝𝕥𝕚𝕓𝕪𝕥𝕖 𝕔𝕙𝕒𝕣𝕒𝕔𝕥𝕖𝕣𝕤' )
+               );
+
+               $this->assertEquals(
+                       'test:BagOStuff-long-key:##dc89dcb43b28614da27660240af478b5',
+                       $this->cache->makeKey( '𝕖𝕧𝕖𝕟', '𝕚𝕗', '𝕨𝕖', '𝕄𝔻𝟝', '𝕖𝕒𝕔𝕙',
+                               '𝕒𝕣𝕘𝕦𝕞𝕖𝕟𝕥', '𝕥𝕙𝕚𝕤', '𝕜𝕖𝕪', '𝕨𝕠𝕦𝕝𝕕', '𝕤𝕥𝕚𝕝𝕝', '𝕓𝕖', '𝕥𝕠𝕠', '𝕝𝕠𝕟𝕘' )
+               );
+
+               $this->assertEquals(
+                       'test:%23%235820ad1d105aa4dc698585c39df73e19',
+                       $this->cache->makeKey( '##5820ad1d105aa4dc698585c39df73e19' )
+               );
+
+               $this->assertEquals(
+                       'test:percent_is_escaped:!@$%25^&*()',
+                       $this->cache->makeKey( 'percent_is_escaped', '!@$%^&*()' )
+               );
+
+               $this->assertEquals(
+                       'test:colon_is_escaped:!@$%3A^&*()',
+                       $this->cache->makeKey( 'colon_is_escaped', '!@$:^&*()' )
+               );
+
+               $this->assertEquals(
+                       'test:long_key_part_hashed:#0244f7b1811d982dd932dd7de01465ac',
+                       $this->cache->makeKey( 'long_key_part_hashed', str_repeat( 'y', 500 ) )
+               );
+       }
+
+       /**
+        * @dataProvider validKeyProvider
+        * @covers MemcachedBagOStuff::validateKeyEncoding
+        */
+       public function testValidateKeyEncoding( $key ) {
+               $this->assertSame( $key, $this->cache->validateKeyEncoding( $key ) );
+       }
+
+       public function validKeyProvider() {
+               return [
+                       'empty' => [ '' ],
+                       'digits' => [ '09' ],
+                       'letters' => [ 'AZaz' ],
+                       'ASCII special characters' => [ '!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~' ],
+               ];
+       }
+
+       /**
+        * @dataProvider invalidKeyProvider
+        * @covers MemcachedBagOStuff::validateKeyEncoding
+        */
+       public function testValidateKeyEncodingThrowsException( $key ) {
+               $this->setExpectedException( Exception::class );
+               $this->cache->validateKeyEncoding( $key );
+       }
+
+       public function invalidKeyProvider() {
+               return [
+                       [ "\x00" ],
+                       [ ' ' ],
+                       [ "\x1F" ],
+                       [ "\x7F" ],
+                       [ "\x80" ],
+                       [ "\xFF" ],
+               ];
+       }
+}
diff --git a/tests/phpunit/unit/includes/objectcache/RESTBagOStuffTest.php b/tests/phpunit/unit/includes/objectcache/RESTBagOStuffTest.php
new file mode 100644 (file)
index 0000000..459e3ee
--- /dev/null
@@ -0,0 +1,96 @@
+<?php
+/**
+ * @group BagOStuff
+ *
+ * @covers RESTBagOStuff
+ */
+class RESTBagOStuffTest extends \MediaWikiUnitTestCase {
+
+       /**
+        * @var MultiHttpClient
+        */
+       private $client;
+       /**
+        * @var RESTBagOStuff
+        */
+       private $bag;
+
+       public function setUp() {
+               parent::setUp();
+               $this->client =
+                       $this->getMockBuilder( MultiHttpClient::class )
+                               ->setConstructorArgs( [ [] ] )
+                               ->setMethods( [ 'run' ] )
+                               ->getMock();
+               $this->bag = new RESTBagOStuff( [ 'client' => $this->client, 'url' => 'http://test/rest/' ] );
+       }
+
+       public function testGet() {
+               $this->client->expects( $this->once() )->method( 'run' )->with( [
+                       'method' => 'GET',
+                       'url' => 'http://test/rest/42xyz42',
+                       'headers' => []
+                       // list( $rcode, $rdesc, $rhdrs, $rbody, $rerr )
+               ] )->willReturn( [ 200, 'OK', [], '"somedata"', 0 ] );
+               $result = $this->bag->get( '42xyz42' );
+               $this->assertEquals( 'somedata', $result );
+       }
+
+       public function testGetNotExist() {
+               $this->client->expects( $this->once() )->method( 'run' )->with( [
+                       'method' => 'GET',
+                       'url' => 'http://test/rest/42xyz42',
+                       'headers' => []
+                       // list( $rcode, $rdesc, $rhdrs, $rbody, $rerr )
+               ] )->willReturn( [ 404, 'Not found', [], 'Nothing to see here', 0 ] );
+               $result = $this->bag->get( '42xyz42' );
+               $this->assertFalse( $result );
+       }
+
+       public function testGetBadClient() {
+               $this->client->expects( $this->once() )->method( 'run' )->with( [
+                       'method' => 'GET',
+                       'url' => 'http://test/rest/42xyz42',
+                       'headers' => []
+                       // list( $rcode, $rdesc, $rhdrs, $rbody, $rerr )
+               ] )->willReturn( [ 0, '', [], '', 'cURL has failed you today' ] );
+               $result = $this->bag->get( '42xyz42' );
+               $this->assertFalse( $result );
+               $this->assertEquals( BagOStuff::ERR_UNREACHABLE, $this->bag->getLastError() );
+       }
+
+       public function testGetBadServer() {
+               $this->client->expects( $this->once() )->method( 'run' )->with( [
+                       'method' => 'GET',
+                       'url' => 'http://test/rest/42xyz42',
+                       'headers' => []
+                       // list( $rcode, $rdesc, $rhdrs, $rbody, $rerr )
+               ] )->willReturn( [ 500, 'Too busy', [], 'Server is too busy', '' ] );
+               $result = $this->bag->get( '42xyz42' );
+               $this->assertFalse( $result );
+               $this->assertEquals( BagOStuff::ERR_UNEXPECTED, $this->bag->getLastError() );
+       }
+
+       public function testPut() {
+               $this->client->expects( $this->once() )->method( 'run' )->with( [
+                       'method' => 'PUT',
+                       'url' => 'http://test/rest/42xyz42',
+                       'body' => '"postdata"',
+                       'headers' => []
+                       // list( $rcode, $rdesc, $rhdrs, $rbody, $rerr )
+               ] )->willReturn( [ 200, 'OK', [], 'Done', 0 ] );
+               $result = $this->bag->set( '42xyz42', 'postdata' );
+               $this->assertTrue( $result );
+       }
+
+       public function testDelete() {
+               $this->client->expects( $this->once() )->method( 'run' )->with( [
+                       'method' => 'DELETE',
+                       'url' => 'http://test/rest/42xyz42',
+                       'headers' => []
+                       // list( $rcode, $rdesc, $rhdrs, $rbody, $rerr )
+               ] )->willReturn( [ 200, 'OK', [], 'Done', 0 ] );
+               $result = $this->bag->delete( '42xyz42' );
+               $this->assertTrue( $result );
+       }
+}
diff --git a/tests/phpunit/unit/includes/objectcache/RedisBagOStuffTest.php b/tests/phpunit/unit/includes/objectcache/RedisBagOStuffTest.php
new file mode 100644 (file)
index 0000000..df5614d
--- /dev/null
@@ -0,0 +1,110 @@
+<?php
+
+use Wikimedia\TestingAccessWrapper;
+
+/**
+ * @group BagOStuff
+ */
+class RedisBagOStuffTest extends PHPUnit\Framework\TestCase {
+
+       use MediaWikiCoversValidator;
+
+       /** @var RedisBagOStuff */
+       private $cache;
+
+       protected function setUp() {
+               parent::setUp();
+               $cache = $this->getMockBuilder( RedisBagOStuff::class )
+                       ->disableOriginalConstructor()
+                       ->getMock();
+               $this->cache = TestingAccessWrapper::newFromObject( $cache );
+       }
+
+       /**
+        * @covers RedisBagOStuff::unserialize
+        * @dataProvider unserializeProvider
+        */
+       public function testUnserialize( $expected, $input, $message ) {
+               $actual = $this->cache->unserialize( $input );
+               $this->assertSame( $expected, $actual, $message );
+       }
+
+       public function unserializeProvider() {
+               return [
+                       [
+                               -1,
+                               '-1',
+                               'String representation of \'-1\'',
+                       ],
+                       [
+                               0,
+                               '0',
+                               'String representation of \'0\'',
+                       ],
+                       [
+                               1,
+                               '1',
+                               'String representation of \'1\'',
+                       ],
+                       [
+                               -1.0,
+                               'd:-1;',
+                               'Serialized negative double',
+                       ],
+                       [
+                               'foo',
+                               's:3:"foo";',
+                               'Serialized string',
+                       ]
+               ];
+       }
+
+       /**
+        * @covers RedisBagOStuff::serialize
+        * @dataProvider serializeProvider
+        */
+       public function testSerialize( $expected, $input, $message ) {
+               $actual = $this->cache->serialize( $input );
+               $this->assertSame( $expected, $actual, $message );
+       }
+
+       public function serializeProvider() {
+               return [
+                       [
+                               -1,
+                               -1,
+                               '-1 as integer',
+                       ],
+                       [
+                               0,
+                               0,
+                               '0 as integer',
+                       ],
+                       [
+                               1,
+                               1,
+                               '1 as integer',
+                       ],
+                       [
+                               'd:-1;',
+                               -1.0,
+                               'Negative double',
+                       ],
+                       [
+                               's:3:"2.1";',
+                               '2.1',
+                               'Decimal string',
+                       ],
+                       [
+                               's:1:"1";',
+                               '1',
+                               'String representation of 1',
+                       ],
+                       [
+                               's:3:"foo";',
+                               'foo',
+                               'String',
+                       ],
+               ];
+       }
+}
diff --git a/tests/phpunit/unit/includes/page/ArticleTest.php b/tests/phpunit/unit/includes/page/ArticleTest.php
new file mode 100644 (file)
index 0000000..61fb4b6
--- /dev/null
@@ -0,0 +1,57 @@
+<?php
+
+class ArticleTest extends \MediaWikiUnitTestCase {
+
+       /**
+        * @var Title
+        */
+       private $title;
+       /**
+        * @var Article
+        */
+       private $article;
+
+       /** creates a title object and its article object */
+       protected function setUp() {
+               parent::setUp();
+               $this->title = Title::makeTitle( NS_MAIN, 'SomePage' );
+               $this->article = new Article( $this->title );
+       }
+
+       /** cleanup title object and its article object */
+       protected function tearDown() {
+               parent::tearDown();
+               $this->title = null;
+               $this->article = null;
+       }
+
+       /**
+        * @covers Article::__get
+        */
+       public function testImplementsGetMagic() {
+               $this->assertEquals( false, $this->article->mLatest, "Article __get magic" );
+       }
+
+       /**
+        * @depends testImplementsGetMagic
+        * @covers Article::__set
+        */
+       public function testImplementsSetMagic() {
+               $this->article->mLatest = 2;
+               $this->assertEquals( 2, $this->article->mLatest, "Article __set magic" );
+       }
+
+       /**
+        * @covers Article::__get
+        * @covers Article::__set
+        */
+       public function testGetOrSetOnNewProperty() {
+               $this->article->ext_someNewProperty = 12;
+               $this->assertEquals( 12, $this->article->ext_someNewProperty,
+                       "Article get/set magic on new field" );
+
+               $this->article->ext_someNewProperty = -8;
+               $this->assertEquals( -8, $this->article->ext_someNewProperty,
+                       "Article get/set magic on update to new field" );
+       }
+}
diff --git a/tests/phpunit/unit/includes/parser/ParserPreloadTest.php b/tests/phpunit/unit/includes/parser/ParserPreloadTest.php
new file mode 100644 (file)
index 0000000..46f07e5
--- /dev/null
@@ -0,0 +1,97 @@
+<?php
+
+use MediaWiki\MediaWikiServices;
+
+/**
+ * Basic tests for Parser::getPreloadText
+ * @author Antoine Musso
+ *
+ * @covers Parser
+ * @covers StripState
+ *
+ * @covers Preprocessor_DOM
+ * @covers PPDStack
+ * @covers PPDStackElement
+ * @covers PPDPart
+ * @covers PPFrame_DOM
+ * @covers PPTemplateFrame_DOM
+ * @covers PPCustomFrame_DOM
+ * @covers PPNode_DOM
+ *
+ * @covers Preprocessor_Hash
+ * @covers PPDStack_Hash
+ * @covers PPDStackElement_Hash
+ * @covers PPDPart_Hash
+ * @covers PPFrame_Hash
+ * @covers PPTemplateFrame_Hash
+ * @covers PPCustomFrame_Hash
+ * @covers PPNode_Hash_Tree
+ * @covers PPNode_Hash_Text
+ * @covers PPNode_Hash_Array
+ * @covers PPNode_Hash_Attr
+ */
+class ParserPreloadTest extends \MediaWikiUnitTestCase {
+       /**
+        * @var Parser
+        */
+       private $testParser;
+       /**
+        * @var ParserOptions
+        */
+       private $testParserOptions;
+       /**
+        * @var Title
+        */
+       private $title;
+
+       protected function setUp() {
+               parent::setUp();
+               $this->testParserOptions = ParserOptions::newFromUserAndLang( new User,
+                       MediaWikiServices::getInstance()->getContentLanguage() );
+
+               $this->testParser = new Parser();
+               $this->testParser->Options( $this->testParserOptions );
+               $this->testParser->clearState();
+
+               $this->title = Title::newFromText( 'Preload Test' );
+       }
+
+       protected function tearDown() {
+               parent::tearDown();
+
+               unset( $this->testParser );
+               unset( $this->title );
+       }
+
+       public function testPreloadSimpleText() {
+               $this->assertPreloaded( 'simple', 'simple' );
+       }
+
+       public function testPreloadedPreIsUnstripped() {
+               $this->assertPreloaded(
+                       '<pre>monospaced</pre>',
+                       '<pre>monospaced</pre>',
+                       '<pre> in preloaded text must be unstripped (T29467)'
+               );
+       }
+
+       public function testPreloadedNowikiIsUnstripped() {
+               $this->assertPreloaded(
+                       '<nowiki>[[Dummy title]]</nowiki>',
+                       '<nowiki>[[Dummy title]]</nowiki>',
+                       '<nowiki> in preloaded text must be unstripped (T29467)'
+               );
+       }
+
+       protected function assertPreloaded( $expected, $text, $msg = '' ) {
+               $this->assertEquals(
+                       $expected,
+                       $this->testParser->getPreloadText(
+                               $text,
+                               $this->title,
+                               $this->testParserOptions
+                       ),
+                       $msg
+               );
+       }
+}
diff --git a/tests/phpunit/unit/includes/parser/PreprocessorTest.php b/tests/phpunit/unit/includes/parser/PreprocessorTest.php
new file mode 100644 (file)
index 0000000..59c3075
--- /dev/null
@@ -0,0 +1,296 @@
+<?php
+
+use MediaWiki\MediaWikiServices;
+
+/**
+ * @covers Preprocessor
+ *
+ * @covers Preprocessor_DOM
+ * @covers PPDStack
+ * @covers PPDStackElement
+ * @covers PPDPart
+ * @covers PPFrame_DOM
+ * @covers PPTemplateFrame_DOM
+ * @covers PPCustomFrame_DOM
+ * @covers PPNode_DOM
+ *
+ * @covers Preprocessor_Hash
+ * @covers PPDStack_Hash
+ * @covers PPDStackElement_Hash
+ * @covers PPDPart_Hash
+ * @covers PPFrame_Hash
+ * @covers PPTemplateFrame_Hash
+ * @covers PPCustomFrame_Hash
+ * @covers PPNode_Hash_Tree
+ * @covers PPNode_Hash_Text
+ * @covers PPNode_Hash_Array
+ * @covers PPNode_Hash_Attr
+ */
+class PreprocessorTest extends \MediaWikiUnitTestCase {
+       protected $mTitle = 'Page title';
+       protected $mPPNodeCount = 0;
+       /**
+        * @var ParserOptions
+        */
+       protected $mOptions;
+       /**
+        * @var array
+        */
+       protected $mPreprocessors;
+
+       protected static $classNames = [
+               Preprocessor_DOM::class,
+               Preprocessor_Hash::class
+       ];
+
+       protected function setUp() {
+               parent::setUp();
+               $this->mOptions = ParserOptions::newFromUserAndLang( new User,
+                       MediaWikiServices::getInstance()->getContentLanguage() );
+
+               $this->mPreprocessors = [];
+               foreach ( self::$classNames as $className ) {
+                       $this->mPreprocessors[$className] = new $className( $this );
+               }
+       }
+
+       function getStripList() {
+               return [ 'gallery', 'display map' /* Used by Maps, see r80025 CR */, '/foo' ];
+       }
+
+       protected static function addClassArg( $testCases ) {
+               $newTestCases = [];
+               foreach ( self::$classNames as $className ) {
+                       foreach ( $testCases as $testCase ) {
+                               array_unshift( $testCase, $className );
+                               $newTestCases[] = $testCase;
+                       }
+               }
+               return $newTestCases;
+       }
+
+       public static function provideCases() {
+               // phpcs:disable Generic.Files.LineLength
+               return self::addClassArg( [
+                       [ "Foo", "<root>Foo</root>" ],
+                       [ "<!-- Foo -->", "<root><comment>&lt;!-- Foo --&gt;</comment></root>" ],
+                       [ "<!-- Foo --><!-- Bar -->", "<root><comment>&lt;!-- Foo --&gt;</comment><comment>&lt;!-- Bar --&gt;</comment></root>" ],
+                       [ "<!-- Foo -->  <!-- Bar -->", "<root><comment>&lt;!-- Foo --&gt;</comment>  <comment>&lt;!-- Bar --&gt;</comment></root>" ],
+                       [ "<!-- Foo --> \n <!-- Bar -->", "<root><comment>&lt;!-- Foo --&gt;</comment> \n <comment>&lt;!-- Bar --&gt;</comment></root>" ],
+                       [ "<!-- Foo --> \n <!-- Bar -->\n", "<root><comment>&lt;!-- Foo --&gt;</comment> \n<comment> &lt;!-- Bar --&gt;\n</comment></root>" ],
+                       [ "<!-- Foo -->  <!-- Bar -->\n", "<root><comment>&lt;!-- Foo --&gt;</comment>  <comment>&lt;!-- Bar --&gt;</comment>\n</root>" ],
+                       [ "<!-->Bar", "<root><comment>&lt;!--&gt;Bar</comment></root>" ],
+                       [ "<!-- Comment -- comment", "<root><comment>&lt;!-- Comment -- comment</comment></root>" ],
+                       [ "== Foo ==\n  <!-- Bar -->\n== Baz ==\n", "<root><h level=\"2\" i=\"1\">== Foo ==</h>\n<comment>  &lt;!-- Bar --&gt;\n</comment><h level=\"2\" i=\"2\">== Baz ==</h>\n</root>" ],
+                       [ "<gallery/>", "<root><ext><name>gallery</name><attr></attr></ext></root>" ],
+                       [ "Foo <gallery/> Bar", "<root>Foo <ext><name>gallery</name><attr></attr></ext> Bar</root>" ],
+                       [ "<gallery></gallery>", "<root><ext><name>gallery</name><attr></attr><inner></inner><close>&lt;/gallery&gt;</close></ext></root>" ],
+                       [ "<foo> <gallery></gallery>", "<root>&lt;foo&gt; <ext><name>gallery</name><attr></attr><inner></inner><close>&lt;/gallery&gt;</close></ext></root>" ],
+                       [ "<foo> <gallery><gallery></gallery>", "<root>&lt;foo&gt; <ext><name>gallery</name><attr></attr><inner>&lt;gallery&gt;</inner><close>&lt;/gallery&gt;</close></ext></root>" ],
+                       [ "<noinclude> Foo bar </noinclude>", "<root><ignore>&lt;noinclude&gt;</ignore> Foo bar <ignore>&lt;/noinclude&gt;</ignore></root>" ],
+                       [ "<noinclude>\n{{Foo}}\n</noinclude>", "<root><ignore>&lt;noinclude&gt;</ignore>\n<template lineStart=\"1\"><title>Foo</title></template>\n<ignore>&lt;/noinclude&gt;</ignore></root>" ],
+                       [ "<noinclude>\n{{Foo}}\n</noinclude>\n", "<root><ignore>&lt;noinclude&gt;</ignore>\n<template lineStart=\"1\"><title>Foo</title></template>\n<ignore>&lt;/noinclude&gt;</ignore>\n</root>" ],
+                       [ "<gallery>foo bar", "<root>&lt;gallery&gt;foo bar</root>" ],
+                       [ "<{{foo}}>", "<root>&lt;<template><title>foo</title></template>&gt;</root>" ],
+                       [ "<{{{foo}}}>", "<root>&lt;<tplarg><title>foo</title></tplarg>&gt;</root>" ],
+                       [ "<gallery></gallery</gallery>", "<root><ext><name>gallery</name><attr></attr><inner>&lt;/gallery</inner><close>&lt;/gallery&gt;</close></ext></root>" ],
+                       [ "=== Foo === ", "<root><h level=\"3\" i=\"1\">=== Foo === </h></root>" ],
+                       [ "==<!-- -->= Foo === ", "<root><h level=\"2\" i=\"1\">==<comment>&lt;!-- --&gt;</comment>= Foo === </h></root>" ],
+                       [ "=== Foo ==<!-- -->= ", "<root><h level=\"1\" i=\"1\">=== Foo ==<comment>&lt;!-- --&gt;</comment>= </h></root>" ],
+                       [ "=== Foo ===<!-- -->\n", "<root><h level=\"3\" i=\"1\">=== Foo ===<comment>&lt;!-- --&gt;</comment></h>\n</root>" ],
+                       [ "=== Foo ===<!-- --> <!-- -->\n", "<root><h level=\"3\" i=\"1\">=== Foo ===<comment>&lt;!-- --&gt;</comment> <comment>&lt;!-- --&gt;</comment></h>\n</root>" ],
+                       [ "== Foo ==\n== Bar == \n", "<root><h level=\"2\" i=\"1\">== Foo ==</h>\n<h level=\"2\" i=\"2\">== Bar == </h>\n</root>" ],
+                       [ "===========", "<root><h level=\"5\" i=\"1\">===========</h></root>" ],
+                       [ "Foo\n=\n==\n=\n", "<root>Foo\n=\n==\n=\n</root>" ],
+                       [ "{{Foo}}", "<root><template><title>Foo</title></template></root>" ],
+                       [ "\n{{Foo}}", "<root>\n<template lineStart=\"1\"><title>Foo</title></template></root>" ],
+                       [ "{{Foo|bar}}", "<root><template><title>Foo</title><part><name index=\"1\" /><value>bar</value></part></template></root>" ],
+                       [ "{{Foo|bar}}a", "<root><template><title>Foo</title><part><name index=\"1\" /><value>bar</value></part></template>a</root>" ],
+                       [ "{{Foo|bar|baz}}", "<root><template><title>Foo</title><part><name index=\"1\" /><value>bar</value></part><part><name index=\"2\" /><value>baz</value></part></template></root>" ],
+                       [ "{{Foo|1=bar}}", "<root><template><title>Foo</title><part><name>1</name>=<value>bar</value></part></template></root>" ],
+                       [ "{{Foo|=bar}}", "<root><template><title>Foo</title><part><name></name>=<value>bar</value></part></template></root>" ],
+                       [ "{{Foo|bar=baz}}", "<root><template><title>Foo</title><part><name>bar</name>=<value>baz</value></part></template></root>" ],
+                       [ "{{Foo|{{bar}}=baz}}", "<root><template><title>Foo</title><part><name><template><title>bar</title></template></name>=<value>baz</value></part></template></root>" ],
+                       [ "{{Foo|1=bar|baz}}", "<root><template><title>Foo</title><part><name>1</name>=<value>bar</value></part><part><name index=\"1\" /><value>baz</value></part></template></root>" ],
+                       [ "{{Foo|1=bar|2=baz}}", "<root><template><title>Foo</title><part><name>1</name>=<value>bar</value></part><part><name>2</name>=<value>baz</value></part></template></root>" ],
+                       [ "{{Foo|bar|foo=baz}}", "<root><template><title>Foo</title><part><name index=\"1\" /><value>bar</value></part><part><name>foo</name>=<value>baz</value></part></template></root>" ],
+                       [ "{{{1}}}", "<root><tplarg><title>1</title></tplarg></root>" ],
+                       [ "{{{1|}}}", "<root><tplarg><title>1</title><part><name index=\"1\" /><value></value></part></tplarg></root>" ],
+                       [ "{{{Foo}}}", "<root><tplarg><title>Foo</title></tplarg></root>" ],
+                       [ "{{{Foo|}}}", "<root><tplarg><title>Foo</title><part><name index=\"1\" /><value></value></part></tplarg></root>" ],
+                       [ "{{{Foo|bar|baz}}}", "<root><tplarg><title>Foo</title><part><name index=\"1\" /><value>bar</value></part><part><name index=\"2\" /><value>baz</value></part></tplarg></root>" ],
+                       [ "{<!-- -->{Foo}}", "<root>{<comment>&lt;!-- --&gt;</comment>{Foo}}</root>" ],
+                       [ "{{{{Foobar}}}}", "<root>{<tplarg><title>Foobar</title></tplarg>}</root>" ],
+                       [ "{{{ {{Foo}} }}}", "<root><tplarg><title> <template><title>Foo</title></template> </title></tplarg></root>" ],
+                       [ "{{ {{{Foo}}} }}", "<root><template><title> <tplarg><title>Foo</title></tplarg> </title></template></root>" ],
+                       [ "{{{{{Foo}}}}}", "<root><template><title><tplarg><title>Foo</title></tplarg></title></template></root>" ],
+                       [ "{{{{{Foo}} }}}", "<root><tplarg><title><template><title>Foo</title></template> </title></tplarg></root>" ],
+                       [ "{{{{{{Foo}}}}}}", "<root><tplarg><title><tplarg><title>Foo</title></tplarg></title></tplarg></root>" ],
+                       [ "{{{{{{Foo}}}}}", "<root>{<template><title><tplarg><title>Foo</title></tplarg></title></template></root>" ],
+                       [ "[[[Foo]]", "<root>[[[Foo]]</root>" ],
+                       [ "{{Foo|[[[[bar]]|baz]]}}", "<root><template><title>Foo</title><part><name index=\"1\" /><value>[[[[bar]]|baz]]</value></part></template></root>" ], // This test is important, since it means the difference between having the [[ rule stacked or not
+                       [ "{{Foo|[[[[bar]|baz]]}}", "<root>{{Foo|[[[[bar]|baz]]}}</root>" ],
+                       [ "{{Foo|Foo [[[[bar]|baz]]}}", "<root>{{Foo|Foo [[[[bar]|baz]]}}</root>" ],
+                       [ "Foo <display map>Bar</display map             >Baz", "<root>Foo <ext><name>display map</name><attr></attr><inner>Bar</inner><close>&lt;/display map             &gt;</close></ext>Baz</root>" ],
+                       [ "Foo <display map foo>Bar</display map             >Baz", "<root>Foo <ext><name>display map</name><attr> foo</attr><inner>Bar</inner><close>&lt;/display map             &gt;</close></ext>Baz</root>" ],
+                       [ "Foo <gallery bar=\"baz\" />", "<root>Foo <ext><name>gallery</name><attr> bar=&quot;baz&quot; </attr></ext></root>" ],
+                       [ "Foo <gallery bar=\"1\" baz=2 />", "<root>Foo <ext><name>gallery</name><attr> bar=&quot;1&quot; baz=2 </attr></ext></root>" ],
+                       [ "</foo>Foo<//foo>", "<root><ext><name>/foo</name><attr></attr><inner>Foo</inner><close>&lt;//foo&gt;</close></ext></root>" ], # Worth blacklisting IMHO
+                       [ "{{#ifexpr: ({{{1|1}}} = 2) | Foo | Bar }}", "<root><template><title>#ifexpr: (<tplarg><title>1</title><part><name index=\"1\" /><value>1</value></part></tplarg> = 2) </title><part><name index=\"1\" /><value> Foo </value></part><part><name index=\"2\" /><value> Bar </value></part></template></root>" ],
+                       [ "{{#if: {{{1|}}} | Foo | {{Bar}} }}", "<root><template><title>#if: <tplarg><title>1</title><part><name index=\"1\" /><value></value></part></tplarg> </title><part><name index=\"1\" /><value> Foo </value></part><part><name index=\"2\" /><value> <template><title>Bar</title></template> </value></part></template></root>" ],
+                       [ "{{#if: {{{1|}}} | Foo | [[Bar]] }}", "<root><template><title>#if: <tplarg><title>1</title><part><name index=\"1\" /><value></value></part></tplarg> </title><part><name index=\"1\" /><value> Foo </value></part><part><name index=\"2\" /><value> [[Bar]] </value></part></template></root>" ],
+                       [ "{{#if: {{{1|}}} | [[Foo]] | Bar }}", "<root><template><title>#if: <tplarg><title>1</title><part><name index=\"1\" /><value></value></part></tplarg> </title><part><name index=\"1\" /><value> [[Foo]] </value></part><part><name index=\"2\" /><value> Bar </value></part></template></root>" ],
+                       [ "{{#if: {{{1|}}} | 1 | {{#if: {{{1|}}} | 2 | 3 }} }}", "<root><template><title>#if: <tplarg><title>1</title><part><name index=\"1\" /><value></value></part></tplarg> </title><part><name index=\"1\" /><value> 1 </value></part><part><name index=\"2\" /><value> <template><title>#if: <tplarg><title>1</title><part><name index=\"1\" /><value></value></part></tplarg> </title><part><name index=\"1\" /><value> 2 </value></part><part><name index=\"2\" /><value> 3 </value></part></template> </value></part></template></root>" ],
+                       [ "{{ {{Foo}}", "<root>{{ <template><title>Foo</title></template></root>" ],
+                       [ "{{Foobar {{Foo}} {{Bar}} {{Baz}} ", "<root>{{Foobar <template><title>Foo</title></template> <template><title>Bar</title></template> <template><title>Baz</title></template> </root>" ],
+                       [ "[[Foo]] |", "<root>[[Foo]] |</root>" ],
+                       [ "{{Foo|Bar|", "<root>{{Foo|Bar|</root>" ],
+                       [ "[[Foo]", "<root>[[Foo]</root>" ],
+                       [ "[[Foo|Bar]", "<root>[[Foo|Bar]</root>" ],
+                       [ "{{Foo| [[Bar] }}", "<root>{{Foo| [[Bar] }}</root>" ],
+                       [ "{{Foo| [[Bar|Baz] }}", "<root>{{Foo| [[Bar|Baz] }}</root>" ],
+                       [ "{{Foo|bar=[[baz]}}", "<root>{{Foo|bar=[[baz]}}</root>" ],
+                       [ "{{foo|", "<root>{{foo|</root>" ],
+                       [ "{{foo|}", "<root>{{foo|}</root>" ],
+                       [ "{{foo|} }}", "<root><template><title>foo</title><part><name index=\"1\" /><value>} </value></part></template></root>" ],
+                       [ "{{foo|bar=|}", "<root>{{foo|bar=|}</root>" ],
+                       [ "{{Foo|} Bar=", "<root>{{Foo|} Bar=</root>" ],
+                       [ "{{Foo|} Bar=}}", "<root><template><title>Foo</title><part><name>} Bar</name>=<value></value></part></template></root>" ],
+                       /* [ file_get_contents( __DIR__ . '/QuoteQuran.txt' ], file_get_contents( __DIR__ . '/QuoteQuranExpanded.txt' ) ], */
+               ] );
+               // phpcs:enable
+       }
+
+       /**
+        * Get XML preprocessor tree from the preprocessor (which may not be the
+        * native XML-based one).
+        *
+        * @param string $className
+        * @param string $wikiText
+        * @return string
+        */
+       protected function preprocessToXml( $className, $wikiText ) {
+               $preprocessor = $this->mPreprocessors[$className];
+               if ( method_exists( $preprocessor, 'preprocessToXml' ) ) {
+                       return $this->normalizeXml( $preprocessor->preprocessToXml( $wikiText ) );
+               }
+
+               $dom = $preprocessor->preprocessToObj( $wikiText );
+               if ( is_callable( [ $dom, 'saveXML' ] ) ) {
+                       return $dom->saveXML();
+               } else {
+                       return $this->normalizeXml( $dom->__toString() );
+               }
+       }
+
+       /**
+        * Normalize XML string to the form that a DOMDocument saves out.
+        *
+        * @param string $xml
+        * @return string
+        */
+       protected function normalizeXml( $xml ) {
+               // Normalize self-closing tags
+               $xml = preg_replace( '!<([a-z]+)/>!', '<$1></$1>', str_replace( ' />', '/>', $xml ) );
+               // Remove <equals> tags, which only occur in Preprocessor_Hash and
+               // have no semantic value
+               $xml = preg_replace( '!</?equals>!', '', $xml );
+               return $xml;
+       }
+
+       /**
+        * @dataProvider provideCases
+        */
+       public function testPreprocessorOutput( $className, $wikiText, $expectedXml ) {
+               $this->assertEquals( $this->normalizeXml( $expectedXml ),
+                       $this->preprocessToXml( $className, $wikiText ) );
+       }
+
+       /**
+        * These are more complex test cases taken out of wiki articles.
+        */
+       public static function provideFiles() {
+               // phpcs:disable Generic.Files.LineLength
+               return self::addClassArg( [
+                       [ "QuoteQuran" ], # https://en.wikipedia.org/w/index.php?title=Template:QuoteQuran/sandbox&oldid=237348988 GFDL + CC BY-SA by Striver
+                       [ "Factorial" ], # https://en.wikipedia.org/w/index.php?title=Template:Factorial&oldid=98548758 GFDL + CC BY-SA by Polonium
+                       [ "All_system_messages" ], # https://tl.wiktionary.org/w/index.php?title=Suleras:All_system_messages&oldid=2765 GPL text generated by MediaWiki
+                       [ "Fundraising" ], # https://tl.wiktionary.org/w/index.php?title=MediaWiki:Sitenotice&oldid=5716 GFDL + CC BY-SA, copied there by Sky Harbor.
+                       [ "NestedTemplates" ], # T29936
+               ] );
+               // phpcs:enable
+       }
+
+       /**
+        * @dataProvider provideFiles
+        */
+       public function testPreprocessorOutputFiles( $className, $filename ) {
+               $folder = __DIR__ . "/../../../../parser/preprocess";
+               $wikiText = file_get_contents( "$folder/$filename.txt" );
+               $output = $this->preprocessToXml( $className, $wikiText );
+
+               $expectedFilename = "$folder/$filename.expected";
+               if ( file_exists( $expectedFilename ) ) {
+                       $expectedXml = $this->normalizeXml( file_get_contents( $expectedFilename ) );
+                       $this->assertEquals( $expectedXml, $output );
+               } else {
+                       $tempFilename = tempnam( $folder, "$filename." );
+                       file_put_contents( $tempFilename, $output );
+                       $this->markTestIncomplete( "File $expectedFilename missing. Output stored as $tempFilename" );
+               }
+       }
+
+       /**
+        * Tests from T30642 · https://phabricator.wikimedia.org/T30642
+        */
+       public static function provideHeadings() {
+               // phpcs:disable Generic.Files.LineLength
+               return self::addClassArg( [
+                       /* These should become headings: */
+                       [ "== h ==<!--c1-->", "<root><h level=\"2\" i=\"1\">== h ==<comment>&lt;!--c1--&gt;</comment></h></root>" ],
+                       [ "== h ==      <!--c1-->", "<root><h level=\"2\" i=\"1\">== h ==       <comment>&lt;!--c1--&gt;</comment></h></root>" ],
+                       [ "== h ==<!--c1-->     ", "<root><h level=\"2\" i=\"1\">== h ==<comment>&lt;!--c1--&gt;</comment>      </h></root>" ],
+                       [ "== h ==      <!--c1-->       ", "<root><h level=\"2\" i=\"1\">== h ==        <comment>&lt;!--c1--&gt;</comment>      </h></root>" ],
+                       [ "== h ==<!--c1--><!--c2-->", "<root><h level=\"2\" i=\"1\">== h ==<comment>&lt;!--c1--&gt;</comment><comment>&lt;!--c2--&gt;</comment></h></root>" ],
+                       [ "== h ==      <!--c1--><!--c2-->", "<root><h level=\"2\" i=\"1\">== h ==      <comment>&lt;!--c1--&gt;</comment><comment>&lt;!--c2--&gt;</comment></h></root>" ],
+                       [ "== h ==<!--c1--><!--c2-->    ", "<root><h level=\"2\" i=\"1\">== h ==<comment>&lt;!--c1--&gt;</comment><comment>&lt;!--c2--&gt;</comment>    </h></root>" ],
+                       [ "== h ==      <!--c1--><!--c2-->      ", "<root><h level=\"2\" i=\"1\">== h ==        <comment>&lt;!--c1--&gt;</comment><comment>&lt;!--c2--&gt;</comment>    </h></root>" ],
+                       [ "== h ==      <!--c1-->  <!--c2-->", "<root><h level=\"2\" i=\"1\">== h ==    <comment>&lt;!--c1--&gt;</comment>  <comment>&lt;!--c2--&gt;</comment></h></root>" ],
+                       [ "== h ==<!--c1-->  <!--c2-->  ", "<root><h level=\"2\" i=\"1\">== h ==<comment>&lt;!--c1--&gt;</comment>  <comment>&lt;!--c2--&gt;</comment>  </h></root>" ],
+                       [ "== h ==      <!--c1-->  <!--c2-->    ", "<root><h level=\"2\" i=\"1\">== h ==        <comment>&lt;!--c1--&gt;</comment>  <comment>&lt;!--c2--&gt;</comment>  </h></root>" ],
+                       [ "== h ==<!--c1--><!--c2--><!--c3-->", "<root><h level=\"2\" i=\"1\">== h ==<comment>&lt;!--c1--&gt;</comment><comment>&lt;!--c2--&gt;</comment><comment>&lt;!--c3--&gt;</comment></h></root>" ],
+                       [ "== h ==<!--c1-->  <!--c2--><!--c3-->", "<root><h level=\"2\" i=\"1\">== h ==<comment>&lt;!--c1--&gt;</comment>  <comment>&lt;!--c2--&gt;</comment><comment>&lt;!--c3--&gt;</comment></h></root>" ],
+                       [ "== h ==<!--c1--><!--c2-->  <!--c3-->", "<root><h level=\"2\" i=\"1\">== h ==<comment>&lt;!--c1--&gt;</comment><comment>&lt;!--c2--&gt;</comment>  <comment>&lt;!--c3--&gt;</comment></h></root>" ],
+                       [ "== h ==<!--c1-->  <!--c2-->  <!--c3-->", "<root><h level=\"2\" i=\"1\">== h ==<comment>&lt;!--c1--&gt;</comment>  <comment>&lt;!--c2--&gt;</comment>  <comment>&lt;!--c3--&gt;</comment></h></root>" ],
+                       [ "== h ==  <!--c1--><!--c2--><!--c3-->", "<root><h level=\"2\" i=\"1\">== h ==  <comment>&lt;!--c1--&gt;</comment><comment>&lt;!--c2--&gt;</comment><comment>&lt;!--c3--&gt;</comment></h></root>" ],
+                       [ "== h ==  <!--c1-->  <!--c2--><!--c3-->", "<root><h level=\"2\" i=\"1\">== h ==  <comment>&lt;!--c1--&gt;</comment>  <comment>&lt;!--c2--&gt;</comment><comment>&lt;!--c3--&gt;</comment></h></root>" ],
+                       [ "== h ==  <!--c1--><!--c2-->  <!--c3-->", "<root><h level=\"2\" i=\"1\">== h ==  <comment>&lt;!--c1--&gt;</comment><comment>&lt;!--c2--&gt;</comment>  <comment>&lt;!--c3--&gt;</comment></h></root>" ],
+                       [ "== h ==  <!--c1-->  <!--c2-->  <!--c3-->", "<root><h level=\"2\" i=\"1\">== h ==  <comment>&lt;!--c1--&gt;</comment>  <comment>&lt;!--c2--&gt;</comment>  <comment>&lt;!--c3--&gt;</comment></h></root>" ],
+                       [ "== h ==<!--c1--><!--c2--><!--c3-->  ", "<root><h level=\"2\" i=\"1\">== h ==<comment>&lt;!--c1--&gt;</comment><comment>&lt;!--c2--&gt;</comment><comment>&lt;!--c3--&gt;</comment>  </h></root>" ],
+                       [ "== h ==<!--c1-->  <!--c2--><!--c3-->  ", "<root><h level=\"2\" i=\"1\">== h ==<comment>&lt;!--c1--&gt;</comment>  <comment>&lt;!--c2--&gt;</comment><comment>&lt;!--c3--&gt;</comment>  </h></root>" ],
+                       [ "== h ==<!--c1--><!--c2-->  <!--c3-->  ", "<root><h level=\"2\" i=\"1\">== h ==<comment>&lt;!--c1--&gt;</comment><comment>&lt;!--c2--&gt;</comment>  <comment>&lt;!--c3--&gt;</comment>  </h></root>" ],
+                       [ "== h ==<!--c1-->  <!--c2-->  <!--c3-->  ", "<root><h level=\"2\" i=\"1\">== h ==<comment>&lt;!--c1--&gt;</comment>  <comment>&lt;!--c2--&gt;</comment>  <comment>&lt;!--c3--&gt;</comment>  </h></root>" ],
+                       [ "== h ==  <!--c1--><!--c2--><!--c3-->  ", "<root><h level=\"2\" i=\"1\">== h ==  <comment>&lt;!--c1--&gt;</comment><comment>&lt;!--c2--&gt;</comment><comment>&lt;!--c3--&gt;</comment>  </h></root>" ],
+                       [ "== h ==  <!--c1-->  <!--c2--><!--c3-->  ", "<root><h level=\"2\" i=\"1\">== h ==  <comment>&lt;!--c1--&gt;</comment>  <comment>&lt;!--c2--&gt;</comment><comment>&lt;!--c3--&gt;</comment>  </h></root>" ],
+                       [ "== h ==  <!--c1--><!--c2-->  <!--c3-->  ", "<root><h level=\"2\" i=\"1\">== h ==  <comment>&lt;!--c1--&gt;</comment><comment>&lt;!--c2--&gt;</comment>  <comment>&lt;!--c3--&gt;</comment>  </h></root>" ],
+                       [ "== h ==  <!--c1-->  <!--c2-->  <!--c3-->  ", "<root><h level=\"2\" i=\"1\">== h ==  <comment>&lt;!--c1--&gt;</comment>  <comment>&lt;!--c2--&gt;</comment>  <comment>&lt;!--c3--&gt;</comment>  </h></root>" ],
+                       [ "== h ==<!--c1-->     <!--c2-->", "<root><h level=\"2\" i=\"1\">== h ==<comment>&lt;!--c1--&gt;</comment>     <comment>&lt;!--c2--&gt;</comment></h></root>" ],
+                       [ "== h ==      <!--c1-->       <!--c2-->", "<root><h level=\"2\" i=\"1\">== h ==       <comment>&lt;!--c1--&gt;</comment>      <comment>&lt;!--c2--&gt;</comment></h></root>" ],
+                       [ "== h ==<!--c1-->     <!--c2-->       ", "<root><h level=\"2\" i=\"1\">== h ==<comment>&lt;!--c1--&gt;</comment>      <comment>&lt;!--c2--&gt;</comment>      </h></root>" ],
+
+                       /* These are not working: */
+                       [ "== h == x <!--c1--><!--c2--><!--c3-->  ", "<root>== h == x <comment>&lt;!--c1--&gt;</comment><comment>&lt;!--c2--&gt;</comment><comment>&lt;!--c3--&gt;</comment>  </root>" ],
+                       [ "== h ==<!--c1--> x <!--c2--><!--c3-->  ", "<root>== h ==<comment>&lt;!--c1--&gt;</comment> x <comment>&lt;!--c2--&gt;</comment><comment>&lt;!--c3--&gt;</comment>  </root>" ],
+                       [ "== h ==<!--c1--><!--c2--><!--c3--> x ", "<root>== h ==<comment>&lt;!--c1--&gt;</comment><comment>&lt;!--c2--&gt;</comment><comment>&lt;!--c3--&gt;</comment> x </root>" ],
+               ] );
+               // phpcs:enable
+       }
+
+       /**
+        * @dataProvider provideHeadings
+        */
+       public function testHeadings( $className, $wikiText, $expectedXml ) {
+               $this->assertEquals( $this->normalizeXml( $expectedXml ),
+                       $this->preprocessToXml( $className, $wikiText ) );
+       }
+}
diff --git a/tests/phpunit/unit/includes/parser/TidyTest.php b/tests/phpunit/unit/includes/parser/TidyTest.php
new file mode 100644 (file)
index 0000000..1adb6a6
--- /dev/null
@@ -0,0 +1,64 @@
+<?php
+
+/**
+ * @group Parser
+ * @covers MWTidy
+ */
+class TidyTest extends \MediaWikiUnitTestCase {
+
+       protected function setUp() {
+               parent::setUp();
+               if ( !MWTidy::isEnabled() ) {
+                       $this->markTestSkipped( 'Tidy not found' );
+               }
+       }
+
+       /**
+        * @dataProvider provideTestWrapping
+        */
+       public function testTidyWrapping( $expected, $text, $msg = '' ) {
+               $text = MWTidy::tidy( $text );
+               // We don't care about where Tidy wants to stick is <p>s
+               $text = trim( preg_replace( '#</?p>#', '', $text ) );
+               // Windows, we love you!
+               $text = str_replace( "\r", '', $text );
+               $this->assertEquals( $expected, $text, $msg );
+       }
+
+       public static function provideTestWrapping() {
+               $testMathML = <<<'MathML'
+<math xmlns="http://www.w3.org/1998/Math/MathML">
+    <mrow>
+      <mi>a</mi>
+      <mo>&InvisibleTimes;</mo>
+      <msup>
+        <mi>x</mi>
+        <mn>2</mn>
+      </msup>
+      <mo>+</mo>
+      <mi>b</mi>
+      <mo>&InvisibleTimes; </mo>
+      <mi>x</mi>
+      <mo>+</mo>
+      <mi>c</mi>
+    </mrow>
+  </math>
+MathML;
+               return [
+                       [
+                               '<mw:editsection page="foo" section="bar">foo</mw:editsection>',
+                               '<mw:editsection page="foo" section="bar">foo</mw:editsection>',
+                               '<mw:editsection> should survive tidy'
+                       ],
+                       [
+                               '<editsection page="foo" section="bar">foo</editsection>',
+                               '<editsection page="foo" section="bar">foo</editsection>',
+                               '<editsection> should survive tidy'
+                       ],
+                       [ '<mw:toc>foo</mw:toc>', '<mw:toc>foo</mw:toc>', '<mw:toc> should survive tidy' ],
+                       [ "<link foo=\"bar\" />foo", '<link foo="bar"/>foo', '<link> should survive tidy' ],
+                       [ "<meta foo=\"bar\" />foo", '<meta foo="bar"/>foo', '<meta> should survive tidy' ],
+                       [ $testMathML, $testMathML, '<math> should survive tidy' ],
+               ];
+       }
+}
diff --git a/tests/phpunit/unit/includes/password/PasswordFactoryTest.php b/tests/phpunit/unit/includes/password/PasswordFactoryTest.php
new file mode 100644 (file)
index 0000000..96e74b1
--- /dev/null
@@ -0,0 +1,124 @@
+<?php
+
+/**
+ * @covers PasswordFactory
+ */
+class PasswordFactoryTest extends \MediaWikiUnitTestCase {
+       public function testConstruct() {
+               $pf = new PasswordFactory();
+               $this->assertEquals( [ '' ], array_keys( $pf->getTypes() ) );
+               $this->assertEquals( '', $pf->getDefaultType() );
+
+               $pf = new PasswordFactory( [
+                       'foo' => [ 'class' => 'FooPassword' ],
+                       'bar' => [ 'class' => 'BarPassword', 'baz' => 'boom' ],
+               ], 'foo' );
+               $this->assertEquals( [ '', 'foo', 'bar' ], array_keys( $pf->getTypes() ) );
+               $this->assertArraySubset( [ 'class' => 'BarPassword', 'baz' => 'boom' ], $pf->getTypes()['bar'] );
+               $this->assertEquals( 'foo', $pf->getDefaultType() );
+       }
+
+       public function testRegister() {
+               $pf = new PasswordFactory;
+               $pf->register( 'foo', [ 'class' => InvalidPassword::class ] );
+               $this->assertArrayHasKey( 'foo', $pf->getTypes() );
+       }
+
+       public function testSetDefaultType() {
+               $pf = new PasswordFactory;
+               $pf->register( '1', [ 'class' => InvalidPassword::class ] );
+               $pf->register( '2', [ 'class' => InvalidPassword::class ] );
+               $pf->setDefaultType( '1' );
+               $this->assertSame( '1', $pf->getDefaultType() );
+               $pf->setDefaultType( '2' );
+               $this->assertSame( '2', $pf->getDefaultType() );
+       }
+
+       /**
+        * @expectedException Exception
+        */
+       public function testSetDefaultTypeError() {
+               $pf = new PasswordFactory;
+               $pf->setDefaultType( 'bogus' );
+       }
+
+       public function testInit() {
+               $config = new HashConfig( [
+                       'PasswordConfig' => [
+                               'foo' => [ 'class' => InvalidPassword::class ],
+                       ],
+                       'PasswordDefault' => 'foo'
+               ] );
+               $pf = new PasswordFactory;
+               $pf->init( $config );
+               $this->assertSame( 'foo', $pf->getDefaultType() );
+               $this->assertArrayHasKey( 'foo', $pf->getTypes() );
+       }
+
+       public function testNewFromCiphertext() {
+               $pf = new PasswordFactory;
+               $pf->register( 'B', [ 'class' => MWSaltedPassword::class ] );
+               $pw = $pf->newFromCiphertext( ':B:salt:d529e941509eb9e9b9cfaeae1fe7ca23' );
+               $this->assertInstanceOf( MWSaltedPassword::class, $pw );
+       }
+
+       public function provideNewFromCiphertextErrors() {
+               return [ [ 'blah' ], [ ':blah:' ] ];
+       }
+
+       /**
+        * @dataProvider provideNewFromCiphertextErrors
+        * @expectedException PasswordError
+        */
+       public function testNewFromCiphertextErrors( $hash ) {
+               $pf = new PasswordFactory;
+               $pf->register( 'B', [ 'class' => MWSaltedPassword::class ] );
+               $pf->newFromCiphertext( $hash );
+       }
+
+       public function testNewFromType() {
+               $pf = new PasswordFactory;
+               $pf->register( 'B', [ 'class' => MWSaltedPassword::class ] );
+               $pw = $pf->newFromType( 'B' );
+               $this->assertInstanceOf( MWSaltedPassword::class, $pw );
+       }
+
+       /**
+        * @expectedException PasswordError
+        */
+       public function testNewFromTypeError() {
+               $pf = new PasswordFactory;
+               $pf->register( 'B', [ 'class' => MWSaltedPassword::class ] );
+               $pf->newFromType( 'bogus' );
+       }
+
+       public function testNewFromPlaintext() {
+               $pf = new PasswordFactory;
+               $pf->register( 'A', [ 'class' => MWOldPassword::class ] );
+               $pf->register( 'B', [ 'class' => MWSaltedPassword::class ] );
+               $pf->setDefaultType( 'A' );
+
+               $this->assertInstanceOf( InvalidPassword::class, $pf->newFromPlaintext( null ) );
+               $this->assertInstanceOf( MWOldPassword::class, $pf->newFromPlaintext( 'password' ) );
+               $this->assertInstanceOf( MWSaltedPassword::class,
+                       $pf->newFromPlaintext( 'password', $pf->newFromType( 'B' ) ) );
+       }
+
+       public function testNeedsUpdate() {
+               $pf = new PasswordFactory;
+               $pf->register( 'A', [ 'class' => MWOldPassword::class ] );
+               $pf->register( 'B', [ 'class' => MWSaltedPassword::class ] );
+               $pf->setDefaultType( 'A' );
+
+               $this->assertFalse( $pf->needsUpdate( $pf->newFromType( 'A' ) ) );
+               $this->assertTrue( $pf->needsUpdate( $pf->newFromType( 'B' ) ) );
+       }
+
+       public function testGenerateRandomPasswordString() {
+               $this->assertSame( 13, strlen( PasswordFactory::generateRandomPasswordString( 13 ) ) );
+       }
+
+       public function testNewInvalidPassword() {
+               $this->assertInstanceOf( InvalidPassword::class, PasswordFactory::newInvalidPassword() );
+       }
+}
diff --git a/tests/phpunit/unit/includes/password/PasswordTest.php b/tests/phpunit/unit/includes/password/PasswordTest.php
new file mode 100644 (file)
index 0000000..b41c0f4
--- /dev/null
@@ -0,0 +1,33 @@
+<?php
+/**
+ * Testing framework for the Password infrastructure
+ *
+ * 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; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * 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.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+/**
+ * @covers InvalidPassword
+ */
+class PasswordTest extends \MediaWikiUnitTestCase {
+       public function testInvalidPlaintext() {
+               $passwordFactory = new PasswordFactory();
+               $invalid = $passwordFactory->newFromPlaintext( null );
+
+               $this->assertInstanceOf( InvalidPassword::class, $invalid );
+       }
+}
diff --git a/tests/phpunit/unit/includes/preferences/FiltersTest.php b/tests/phpunit/unit/includes/preferences/FiltersTest.php
new file mode 100644 (file)
index 0000000..d2b5d05
--- /dev/null
@@ -0,0 +1,141 @@
+<?php
+/**
+ * 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; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * 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.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+use MediaWiki\Preferences\IntvalFilter;
+use MediaWiki\Preferences\MultiUsernameFilter;
+use MediaWiki\Preferences\TimezoneFilter;
+
+/**
+ * @group Preferences
+ */
+class FiltersTest extends \MediaWikiUnitTestCase {
+       /**
+        * @covers MediaWiki\Preferences\IntvalFilter::filterFromForm()
+        * @covers MediaWiki\Preferences\IntvalFilter::filterForForm()
+        */
+       public function testIntvalFilter() {
+               $filter = new IntvalFilter();
+               self::assertSame( 0, $filter->filterFromForm( '0' ) );
+               self::assertSame( 3, $filter->filterFromForm( '3' ) );
+               self::assertSame( '123', $filter->filterForForm( '123' ) );
+       }
+
+       /**
+        * @covers       MediaWiki\Preferences\TimezoneFilter::filterFromForm()
+        * @dataProvider provideTimezoneFilter
+        *
+        * @param string $input
+        * @param string $expected
+        */
+       public function testTimezoneFilter( $input, $expected ) {
+               $filter = new TimezoneFilter();
+               $result = $filter->filterFromForm( $input );
+               self::assertEquals( $expected, $result );
+       }
+
+       public function provideTimezoneFilter() {
+               return [
+                       [ 'ZoneInfo', 'Offset|0' ],
+                       [ 'ZoneInfo|bogus', 'Offset|0' ],
+                       [ 'System', 'System' ],
+                       [ '2:30', 'Offset|150' ],
+               ];
+       }
+
+       /**
+        * @covers MediaWiki\Preferences\MultiUsernameFilter::filterFromForm()
+        * @dataProvider provideMultiUsernameFilterFrom
+        *
+        * @param string $input
+        * @param string|null $expected
+        */
+       public function testMultiUsernameFilterFrom( $input, $expected ) {
+               $filter = $this->makeMultiUsernameFilter();
+               $result = $filter->filterFromForm( $input );
+               self::assertSame( $expected, $result );
+       }
+
+       public function provideMultiUsernameFilterFrom() {
+               return [
+                       [ '', null ],
+                       [ "\n\n\n", null ],
+                       [ 'Foo', '1' ],
+                       [ "\n\n\nFoo\nBar\n", "1\n2" ],
+                       [ "Baz\nInvalid\nFoo", "3\n1" ],
+                       [ "Invalid", null ],
+                       [ "Invalid\n\n\nInvalid\n", null ],
+               ];
+       }
+
+       /**
+        * @covers MediaWiki\Preferences\MultiUsernameFilter::filterForForm()
+        * @dataProvider provideMultiUsernameFilterFor
+        *
+        * @param string $input
+        * @param string $expected
+        */
+       public function testMultiUsernameFilterFor( $input, $expected ) {
+               $filter = $this->makeMultiUsernameFilter();
+               $result = $filter->filterForForm( $input );
+               self::assertSame( $expected, $result );
+       }
+
+       public function provideMultiUsernameFilterFor() {
+               return [
+                       [ '', '' ],
+                       [ "\n", '' ],
+                       [ '1', 'Foo' ],
+                       [ "\n1\n\n2\377\n", "Foo\nBar" ],
+                       [ "666\n667", '' ],
+               ];
+       }
+
+       private function makeMultiUsernameFilter() {
+               $userMapping = [
+                       'Foo' => 1,
+                       'Bar' => 2,
+                       'Baz' => 3,
+               ];
+               $flipped = array_flip( $userMapping );
+               $idLookup = self::getMockBuilder( CentralIdLookup::class )
+                       ->disableOriginalConstructor()
+                       ->setMethods( [ 'centralIdsFromNames', 'namesFromCentralIds' ] )
+                       ->getMockForAbstractClass();
+
+               $idLookup->method( 'centralIdsFromNames' )
+                       ->will( self::returnCallback( function ( $names ) use ( $userMapping ) {
+                               $ids = [];
+                               foreach ( $names as $name ) {
+                                       $ids[] = $userMapping[$name] ?? null;
+                               }
+                               return array_filter( $ids, 'is_numeric' );
+                       } ) );
+               $idLookup->method( 'namesFromCentralIds' )
+                       ->will( self::returnCallback( function ( $ids ) use ( $flipped ) {
+                               $names = [];
+                               foreach ( $ids as $id ) {
+                                       $names[] = $flipped[$id] ?? null;
+                               }
+                               return array_filter( $names, 'is_string' );
+                       } ) );
+
+               return new MultiUsernameFilter( $idLookup );
+       }
+}
diff --git a/tests/phpunit/unit/includes/registration/ExtensionJsonValidatorTest.php b/tests/phpunit/unit/includes/registration/ExtensionJsonValidatorTest.php
new file mode 100644 (file)
index 0000000..77bc23b
--- /dev/null
@@ -0,0 +1,97 @@
+<?php
+/**
+ * Copyright (C) 2018 Kunal Mehta <legoktm@member.fsf.org>
+ *
+ * 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; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * 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.
+ *
+ */
+
+/**
+ * @covers ExtensionJsonValidator
+ */
+class ExtensionJsonValidatorTest extends \MediaWikiUnitTestCase {
+
+       /**
+        * @dataProvider provideValidate
+        */
+       public function testValidate( $file, $expected ) {
+               // If a dependency is missing, skip this test.
+               $validator = new ExtensionJsonValidator( function ( $msg ) {
+                       $this->markTestSkipped( $msg );
+               } );
+
+               if ( is_string( $expected ) ) {
+                       $this->setExpectedException(
+                               ExtensionJsonValidationError::class,
+                               $expected
+                       );
+               }
+
+               $dir = __DIR__ . '/../../../data/registration/';
+               $this->assertSame(
+                       $expected,
+                       $validator->validate( $dir . $file )
+               );
+       }
+
+       public function provideValidate() {
+               return [
+                       [
+                               'notjson.txt',
+                               'notjson.txt is not valid JSON'
+                       ],
+                       [
+                               'duplicate_keys.json',
+                               'Duplicate key: name'
+                       ],
+                       [
+                               'no_manifest_version.json',
+                               'no_manifest_version.json does not have manifest_version set.'
+                       ],
+                       [
+                               'old_manifest_version.json',
+                               'old_manifest_version.json is using a non-supported schema version'
+                       ],
+                       [
+                               'newer_manifest_version.json',
+                               'newer_manifest_version.json is using a non-supported schema version'
+                       ],
+                       [
+                               'bad_spdx.json',
+                               "bad_spdx.json did not pass validation.
+[license-name] Invalid SPDX license identifier, see <https://spdx.org/licenses/>"
+                       ],
+                       [
+                               'invalid.json',
+                               "invalid.json did not pass validation.
+[license-name] Array value found, but a string is required"
+                       ],
+                       [
+                               'good.json',
+                               true
+                       ],
+                       [
+                               'bad_url.json', 'bad_url.json did not pass validation.
+[url] Should use HTTPS for www.mediawiki.org URLs'
+                       ],
+                       [
+                               'bad_url2.json', 'bad_url2.json did not pass validation.
+[url] Should use www.mediawiki.org domain
+[url] Should use HTTPS for www.mediawiki.org URLs'
+                       ]
+               ];
+       }
+
+}
diff --git a/tests/phpunit/unit/includes/registration/ExtensionProcessorTest.php b/tests/phpunit/unit/includes/registration/ExtensionProcessorTest.php
new file mode 100644 (file)
index 0000000..13de142
--- /dev/null
@@ -0,0 +1,829 @@
+<?php
+
+use Wikimedia\TestingAccessWrapper;
+
+/**
+ * @covers ExtensionProcessor
+ */
+class ExtensionProcessorTest extends \MediaWikiUnitTestCase {
+
+       private $dir, $dirname;
+
+       public function setUp() {
+               parent::setUp();
+               $this->dir = __DIR__ . '/FooBar/extension.json';
+               $this->dirname = dirname( $this->dir );
+       }
+
+       /**
+        * 'name' is absolutely required
+        *
+        * @var array
+        */
+       public static $default = [
+               'name' => 'FooBar',
+       ];
+
+       public function testExtractInfo() {
+               // Test that attributes that begin with @ are ignored
+               $processor = new ExtensionProcessor();
+               $processor->extractInfo( $this->dir, self::$default + [
+                       '@metadata' => [ 'foobarbaz' ],
+                       'AnAttribute' => [ 'omg' ],
+                       'AutoloadClasses' => [ 'FooBar' => 'includes/FooBar.php' ],
+                       'SpecialPages' => [ 'Foo' => 'SpecialFoo' ],
+                       'callback' => 'FooBar::onRegistration',
+               ], 1 );
+
+               $extracted = $processor->getExtractedInfo();
+               $attributes = $extracted['attributes'];
+               $this->assertArrayHasKey( 'AnAttribute', $attributes );
+               $this->assertArrayNotHasKey( '@metadata', $attributes );
+               $this->assertArrayNotHasKey( 'AutoloadClasses', $attributes );
+               $this->assertSame(
+                       [ 'FooBar' => 'FooBar::onRegistration' ],
+                       $extracted['callbacks']
+               );
+               $this->assertSame(
+                       [ 'Foo' => 'SpecialFoo' ],
+                       $extracted['globals']['wgSpecialPages']
+               );
+       }
+
+       public function testExtractNamespaces() {
+               // Test that namespace IDs can be overwritten
+               if ( !defined( 'MW_EXTENSION_PROCESSOR_TEST_EXTRACT_INFO_X' ) ) {
+                       define( 'MW_EXTENSION_PROCESSOR_TEST_EXTRACT_INFO_X', 123456 );
+               }
+
+               $processor = new ExtensionProcessor();
+               $processor->extractInfo( $this->dir, self::$default + [
+                       'namespaces' => [
+                               [
+                                       'id' => 332200,
+                                       'constant' => 'MW_EXTENSION_PROCESSOR_TEST_EXTRACT_INFO_A',
+                                       'name' => 'Test_A',
+                                       'defaultcontentmodel' => 'TestModel',
+                                       'gender' => [
+                                               'male' => 'Male test',
+                                               'female' => 'Female test',
+                                       ],
+                                       'subpages' => true,
+                                       'content' => true,
+                                       'protection' => 'userright',
+                               ],
+                               [ // Test_X will use ID 123456 not 334400
+                                       'id' => 334400,
+                                       'constant' => 'MW_EXTENSION_PROCESSOR_TEST_EXTRACT_INFO_X',
+                                       'name' => 'Test_X',
+                                       'defaultcontentmodel' => 'TestModel'
+                               ],
+                       ]
+               ], 1 );
+
+               $extracted = $processor->getExtractedInfo();
+
+               $this->assertArrayHasKey(
+                       'MW_EXTENSION_PROCESSOR_TEST_EXTRACT_INFO_A',
+                       $extracted['defines']
+               );
+               $this->assertArrayNotHasKey(
+                       'MW_EXTENSION_PROCESSOR_TEST_EXTRACT_INFO_X',
+                       $extracted['defines']
+               );
+
+               $this->assertSame(
+                       $extracted['defines']['MW_EXTENSION_PROCESSOR_TEST_EXTRACT_INFO_A'],
+                       332200
+               );
+
+               $this->assertArrayHasKey( 'ExtensionNamespaces', $extracted['attributes'] );
+               $this->assertArrayHasKey( 123456, $extracted['attributes']['ExtensionNamespaces'] );
+               $this->assertArrayHasKey( 332200, $extracted['attributes']['ExtensionNamespaces'] );
+               $this->assertArrayNotHasKey( 334400, $extracted['attributes']['ExtensionNamespaces'] );
+
+               $this->assertSame( 'Test_X', $extracted['attributes']['ExtensionNamespaces'][123456] );
+               $this->assertSame( 'Test_A', $extracted['attributes']['ExtensionNamespaces'][332200] );
+               $this->assertSame(
+                       [ 'male' => 'Male test', 'female' => 'Female test' ],
+                       $extracted['globals']['wgExtraGenderNamespaces'][332200]
+               );
+               // A has subpages, X does not
+               $this->assertTrue( $extracted['globals']['wgNamespacesWithSubpages'][332200] );
+               $this->assertArrayNotHasKey( 123456, $extracted['globals']['wgNamespacesWithSubpages'] );
+       }
+
+       public static function provideRegisterHooks() {
+               $merge = [ ExtensionRegistry::MERGE_STRATEGY => 'array_merge_recursive' ];
+               // Format:
+               // Current $wgHooks
+               // Content in extension.json
+               // Expected value of $wgHooks
+               return [
+                       // No hooks
+                       [
+                               [],
+                               self::$default,
+                               $merge,
+                       ],
+                       // No current hooks, adding one for "FooBaz" in string format
+                       [
+                               [],
+                               [ 'Hooks' => [ 'FooBaz' => 'FooBazCallback' ] ] + self::$default,
+                               [ 'FooBaz' => [ 'FooBazCallback' ] ] + $merge,
+                       ],
+                       // Hook for "FooBaz", adding another one
+                       [
+                               [ 'FooBaz' => [ 'PriorCallback' ] ],
+                               [ 'Hooks' => [ 'FooBaz' => 'FooBazCallback' ] ] + self::$default,
+                               [ 'FooBaz' => [ 'PriorCallback', 'FooBazCallback' ] ] + $merge,
+                       ],
+                       // No current hooks, adding one for "FooBaz" in verbose array format
+                       [
+                               [],
+                               [ 'Hooks' => [ 'FooBaz' => [ 'FooBazCallback' ] ] ] + self::$default,
+                               [ 'FooBaz' => [ 'FooBazCallback' ] ] + $merge,
+                       ],
+                       // Hook for "BarBaz", adding one for "FooBaz"
+                       [
+                               [ 'BarBaz' => [ 'BarBazCallback' ] ],
+                               [ 'Hooks' => [ 'FooBaz' => 'FooBazCallback' ] ] + self::$default,
+                               [
+                                       'BarBaz' => [ 'BarBazCallback' ],
+                                       'FooBaz' => [ 'FooBazCallback' ],
+                               ] + $merge,
+                       ],
+                       // Callbacks for FooBaz wrapped in an array
+                       [
+                               [],
+                               [ 'Hooks' => [ 'FooBaz' => [ 'Callback1' ] ] ] + self::$default,
+                               [
+                                       'FooBaz' => [ 'Callback1' ],
+                               ] + $merge,
+                       ],
+                       // Multiple callbacks for FooBaz hook
+                       [
+                               [],
+                               [ 'Hooks' => [ 'FooBaz' => [ 'Callback1', 'Callback2' ] ] ] + self::$default,
+                               [
+                                       'FooBaz' => [ 'Callback1', 'Callback2' ],
+                               ] + $merge,
+                       ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideRegisterHooks
+        */
+       public function testRegisterHooks( $pre, $info, $expected ) {
+               $processor = new MockExtensionProcessor( [ 'wgHooks' => $pre ] );
+               $processor->extractInfo( $this->dir, $info, 1 );
+               $extracted = $processor->getExtractedInfo();
+               $this->assertEquals( $expected, $extracted['globals']['wgHooks'] );
+       }
+
+       public function testExtractConfig1() {
+               $processor = new ExtensionProcessor;
+               $info = [
+                       'config' => [
+                               'Bar' => 'somevalue',
+                               'Foo' => 10,
+                               '@IGNORED' => 'yes',
+                       ],
+               ] + self::$default;
+               $info2 = [
+                       'config' => [
+                               '_prefix' => 'eg',
+                               'Bar' => 'somevalue'
+                       ],
+                       'name' => 'FooBar2',
+               ];
+               $processor->extractInfo( $this->dir, $info, 1 );
+               $processor->extractInfo( $this->dir, $info2, 1 );
+               $extracted = $processor->getExtractedInfo();
+               $this->assertEquals( 'somevalue', $extracted['globals']['wgBar'] );
+               $this->assertEquals( 10, $extracted['globals']['wgFoo'] );
+               $this->assertArrayNotHasKey( 'wg@IGNORED', $extracted['globals'] );
+               // Custom prefix:
+               $this->assertEquals( 'somevalue', $extracted['globals']['egBar'] );
+       }
+
+       public function testExtractConfig2() {
+               $processor = new ExtensionProcessor;
+               $info = [
+                       'config' => [
+                               'Bar' => [ 'value' => 'somevalue' ],
+                               'Foo' => [ 'value' => 10 ],
+                               'Path' => [ 'value' => 'foo.txt', 'path' => true ],
+                               'Namespaces' => [
+                                       'value' => [
+                                               '10' => true,
+                                               '12' => false,
+                                       ],
+                                       'merge_strategy' => 'array_plus',
+                               ],
+                       ],
+               ] + self::$default;
+               $info2 = [
+                       'config' => [
+                               'Bar' => [ 'value' => 'somevalue' ],
+                       ],
+                       'config_prefix' => 'eg',
+                       'name' => 'FooBar2',
+               ];
+               $processor->extractInfo( $this->dir, $info, 2 );
+               $processor->extractInfo( $this->dir, $info2, 2 );
+               $extracted = $processor->getExtractedInfo();
+               $this->assertEquals( 'somevalue', $extracted['globals']['wgBar'] );
+               $this->assertEquals( 10, $extracted['globals']['wgFoo'] );
+               $this->assertEquals( "{$this->dirname}/foo.txt", $extracted['globals']['wgPath'] );
+               // Custom prefix:
+               $this->assertEquals( 'somevalue', $extracted['globals']['egBar'] );
+               $this->assertSame(
+                       [ 10 => true, 12 => false, ExtensionRegistry::MERGE_STRATEGY => 'array_plus' ],
+                       $extracted['globals']['wgNamespaces']
+               );
+       }
+
+       /**
+        * @expectedException RuntimeException
+        */
+       public function testDuplicateConfigKey1() {
+               $processor = new ExtensionProcessor;
+               $info = [
+                       'config' => [
+                               'Bar' => '',
+                       ]
+               ] + self::$default;
+               $info2 = [
+                       'config' => [
+                               'Bar' => 'g',
+                       ],
+                       'name' => 'FooBar2',
+               ];
+               $processor->extractInfo( $this->dir, $info, 1 );
+               $processor->extractInfo( $this->dir, $info2, 1 );
+       }
+
+       /**
+        * @expectedException RuntimeException
+        */
+       public function testDuplicateConfigKey2() {
+               $processor = new ExtensionProcessor;
+               $info = [
+                       'config' => [
+                               'Bar' => [ 'value' => 'somevalue' ],
+                       ]
+               ] + self::$default;
+               $info2 = [
+                       'config' => [
+                               'Bar' => [ 'value' => 'somevalue' ],
+                       ],
+                       'name' => 'FooBar2',
+               ];
+               $processor->extractInfo( $this->dir, $info, 2 );
+               $processor->extractInfo( $this->dir, $info2, 2 );
+       }
+
+       public static function provideExtractExtensionMessagesFiles() {
+               $dir = __DIR__ . '/FooBar/';
+               return [
+                       [
+                               [ 'ExtensionMessagesFiles' => [ 'FooBarAlias' => 'FooBar.alias.php' ] ],
+                               [ 'wgExtensionMessagesFiles' => [ 'FooBarAlias' => $dir . 'FooBar.alias.php' ] ]
+                       ],
+                       [
+                               [
+                                       'ExtensionMessagesFiles' => [
+                                               'FooBarAlias' => 'FooBar.alias.php',
+                                               'FooBarMagic' => 'FooBar.magic.i18n.php',
+                                       ],
+                               ],
+                               [
+                                       'wgExtensionMessagesFiles' => [
+                                               'FooBarAlias' => $dir . 'FooBar.alias.php',
+                                               'FooBarMagic' => $dir . 'FooBar.magic.i18n.php',
+                                       ],
+                               ],
+                       ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideExtractExtensionMessagesFiles
+        */
+       public function testExtractExtensionMessagesFiles( $input, $expected ) {
+               $processor = new ExtensionProcessor();
+               $processor->extractInfo( $this->dir, $input + self::$default, 1 );
+               $out = $processor->getExtractedInfo();
+               foreach ( $expected as $key => $value ) {
+                       $this->assertEquals( $value, $out['globals'][$key] );
+               }
+       }
+
+       public static function provideExtractMessagesDirs() {
+               $dir = __DIR__ . '/FooBar/';
+               return [
+                       [
+                               [ 'MessagesDirs' => [ 'VisualEditor' => 'i18n' ] ],
+                               [ 'wgMessagesDirs' => [ 'VisualEditor' => [ $dir . 'i18n' ] ] ]
+                       ],
+                       [
+                               [ 'MessagesDirs' => [ 'VisualEditor' => [ 'i18n', 'foobar' ] ] ],
+                               [ 'wgMessagesDirs' => [ 'VisualEditor' => [ $dir . 'i18n', $dir . 'foobar' ] ] ]
+                       ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideExtractMessagesDirs
+        */
+       public function testExtractMessagesDirs( $input, $expected ) {
+               $processor = new ExtensionProcessor();
+               $processor->extractInfo( $this->dir, $input + self::$default, 1 );
+               $out = $processor->getExtractedInfo();
+               foreach ( $expected as $key => $value ) {
+                       $this->assertEquals( $value, $out['globals'][$key] );
+               }
+       }
+
+       public function testExtractCredits() {
+               $processor = new ExtensionProcessor();
+               $processor->extractInfo( $this->dir, self::$default, 1 );
+               $this->setExpectedException( Exception::class );
+               $processor->extractInfo( $this->dir, self::$default, 1 );
+       }
+
+       /**
+        * @dataProvider provideExtractResourceLoaderModules
+        */
+       public function testExtractResourceLoaderModules(
+               $input,
+               array $expectedGlobals,
+               array $expectedAttribs = []
+       ) {
+               $processor = new ExtensionProcessor();
+               $processor->extractInfo( $this->dir, $input + self::$default, 1 );
+               $out = $processor->getExtractedInfo();
+               foreach ( $expectedGlobals as $key => $value ) {
+                       $this->assertEquals( $value, $out['globals'][$key] );
+               }
+               foreach ( $expectedAttribs as $key => $value ) {
+                       $this->assertEquals( $value, $out['attributes'][$key] );
+               }
+       }
+
+       public static function provideExtractResourceLoaderModules() {
+               $dir = __DIR__ . '/FooBar';
+               return [
+                       // Generic module with localBasePath/remoteExtPath specified
+                       [
+                               // Input
+                               [
+                                       'ResourceModules' => [
+                                               'test.foo' => [
+                                                       'styles' => 'foobar.js',
+                                                       'localBasePath' => '',
+                                                       'remoteExtPath' => 'FooBar',
+                                               ],
+                                       ],
+                               ],
+                               // Expected
+                               [
+                                       'wgResourceModules' => [
+                                               'test.foo' => [
+                                                       'styles' => 'foobar.js',
+                                                       'localBasePath' => $dir,
+                                                       'remoteExtPath' => 'FooBar',
+                                               ],
+                                       ],
+                               ],
+                       ],
+                       // ResourceFileModulePaths specified:
+                       [
+                               // Input
+                               [
+                                       'ResourceFileModulePaths' => [
+                                               'localBasePath' => 'modules',
+                                               'remoteExtPath' => 'FooBar/modules',
+                                       ],
+                                       'ResourceModules' => [
+                                               // No paths
+                                               'test.foo' => [
+                                                       'styles' => 'foo.js',
+                                               ],
+                                               // Different paths set
+                                               'test.bar' => [
+                                                       'styles' => 'bar.js',
+                                                       'localBasePath' => 'subdir',
+                                                       'remoteExtPath' => 'FooBar/subdir',
+                                               ],
+                                               // Custom class with no paths set
+                                               'test.class' => [
+                                                       'class' => 'FooBarModule',
+                                                       'extra' => 'argument',
+                                               ],
+                                               // Custom class with a localBasePath
+                                               'test.class.with.path' => [
+                                                       'class' => 'FooBarPathModule',
+                                                       'extra' => 'argument',
+                                                       'localBasePath' => '',
+                                               ]
+                                       ],
+                               ],
+                               // Expected
+                               [
+                                       'wgResourceModules' => [
+                                               'test.foo' => [
+                                                       'styles' => 'foo.js',
+                                                       'localBasePath' => "$dir/modules",
+                                                       'remoteExtPath' => 'FooBar/modules',
+                                               ],
+                                               'test.bar' => [
+                                                       'styles' => 'bar.js',
+                                                       'localBasePath' => "$dir/subdir",
+                                                       'remoteExtPath' => 'FooBar/subdir',
+                                               ],
+                                               'test.class' => [
+                                                       'class' => 'FooBarModule',
+                                                       'extra' => 'argument',
+                                                       'localBasePath' => "$dir/modules",
+                                                       'remoteExtPath' => 'FooBar/modules',
+                                               ],
+                                               'test.class.with.path' => [
+                                                       'class' => 'FooBarPathModule',
+                                                       'extra' => 'argument',
+                                                       'localBasePath' => $dir,
+                                                       'remoteExtPath' => 'FooBar/modules',
+                                               ]
+                                       ],
+                               ],
+                       ],
+                       // ResourceModuleSkinStyles with file module paths
+                       [
+                               // Input
+                               [
+                                       'ResourceFileModulePaths' => [
+                                               'localBasePath' => '',
+                                               'remoteSkinPath' => 'FooBar',
+                                       ],
+                                       'ResourceModuleSkinStyles' => [
+                                               'foobar' => [
+                                                       'test.foo' => 'foo.css',
+                                               ]
+                                       ],
+                               ],
+                               // Expected
+                               [
+                                       'wgResourceModuleSkinStyles' => [
+                                               'foobar' => [
+                                                       'test.foo' => 'foo.css',
+                                                       'localBasePath' => $dir,
+                                                       'remoteSkinPath' => 'FooBar',
+                                               ],
+                                       ],
+                               ],
+                       ],
+                       // ResourceModuleSkinStyles with file module paths and an override
+                       [
+                               // Input
+                               [
+                                       'ResourceFileModulePaths' => [
+                                               'localBasePath' => '',
+                                               'remoteSkinPath' => 'FooBar',
+                                       ],
+                                       'ResourceModuleSkinStyles' => [
+                                               'foobar' => [
+                                                       'test.foo' => 'foo.css',
+                                                       'remoteSkinPath' => 'BarFoo'
+                                               ],
+                                       ],
+                               ],
+                               // Expected
+                               [
+                                       'wgResourceModuleSkinStyles' => [
+                                               'foobar' => [
+                                                       'test.foo' => 'foo.css',
+                                                       'localBasePath' => $dir,
+                                                       'remoteSkinPath' => 'BarFoo',
+                                               ],
+                                       ],
+                               ],
+                       ],
+                       'QUnit test module' => [
+                               // Input
+                               [
+                                       'QUnitTestModule' => [
+                                               'localBasePath' => '',
+                                               'remoteExtPath' => 'Foo',
+                                               'scripts' => 'bar.js',
+                                       ],
+                               ],
+                               // Expected
+                               [],
+                               [
+                                       'QUnitTestModules' => [
+                                               'test.FooBar' => [
+                                                       'localBasePath' => $dir,
+                                                       'remoteExtPath' => 'Foo',
+                                                       'scripts' => 'bar.js',
+                                               ],
+                                       ],
+                               ],
+                       ],
+               ];
+       }
+
+       public static function provideSetToGlobal() {
+               return [
+                       [
+                               [ 'wgAPIModules', 'wgAvailableRights' ],
+                               [],
+                               [
+                                       'APIModules' => [ 'foobar' => 'ApiFooBar' ],
+                                       'AvailableRights' => [ 'foobar', 'unfoobar' ],
+                               ],
+                               [
+                                       'wgAPIModules' => [ 'foobar' => 'ApiFooBar' ],
+                                       'wgAvailableRights' => [ 'foobar', 'unfoobar' ],
+                               ],
+                       ],
+                       [
+                               [ 'wgAPIModules', 'wgAvailableRights' ],
+                               [
+                                       'wgAPIModules' => [ 'barbaz' => 'ApiBarBaz' ],
+                                       'wgAvailableRights' => [ 'barbaz' ]
+                               ],
+                               [
+                                       'APIModules' => [ 'foobar' => 'ApiFooBar' ],
+                                       'AvailableRights' => [ 'foobar', 'unfoobar' ],
+                               ],
+                               [
+                                       'wgAPIModules' => [ 'barbaz' => 'ApiBarBaz', 'foobar' => 'ApiFooBar' ],
+                                       'wgAvailableRights' => [ 'barbaz', 'foobar', 'unfoobar' ],
+                               ],
+                       ],
+                       [
+                               [ 'wgGroupPermissions' ],
+                               [
+                                       'wgGroupPermissions' => [
+                                               'sysop' => [ 'delete' ]
+                                       ],
+                               ],
+                               [
+                                       'GroupPermissions' => [
+                                               'sysop' => [ 'undelete' ],
+                                               'user' => [ 'edit' ]
+                                       ],
+                               ],
+                               [
+                                       'wgGroupPermissions' => [
+                                               'sysop' => [ 'delete', 'undelete' ],
+                                               'user' => [ 'edit' ]
+                                       ],
+                               ]
+                       ]
+               ];
+       }
+
+       /**
+        * Attributes under manifest_version 2
+        */
+       public function testExtractAttributes() {
+               $processor = new ExtensionProcessor();
+               // Load FooBar extension
+               $processor->extractInfo( $this->dir, [ 'name' => 'FooBar' ], 2 );
+               $processor->extractInfo(
+                       $this->dir,
+                       [
+                               'name' => 'Baz',
+                               'attributes' => [
+                                       // Loaded
+                                       'FooBar' => [
+                                               'Plugins' => [
+                                                       'ext.baz.foobar',
+                                               ],
+                                       ],
+                                       // Not loaded
+                                       'FizzBuzz' => [
+                                               'MorePlugins' => [
+                                                       'ext.baz.fizzbuzz',
+                                               ],
+                                       ],
+                               ],
+                       ],
+                       2
+               );
+
+               $info = $processor->getExtractedInfo();
+               $this->assertArrayHasKey( 'FooBarPlugins', $info['attributes'] );
+               $this->assertSame( [ 'ext.baz.foobar' ], $info['attributes']['FooBarPlugins'] );
+               $this->assertArrayNotHasKey( 'FizzBuzzMorePlugins', $info['attributes'] );
+       }
+
+       /**
+        * Attributes under manifest_version 1
+        */
+       public function testAttributes1() {
+               $processor = new ExtensionProcessor();
+               $processor->extractInfo(
+                       $this->dir,
+                       [
+                               'name' => 'FooBar',
+                               'FooBarPlugins' => [
+                                       'ext.baz.foobar',
+                               ],
+                               'FizzBuzzMorePlugins' => [
+                                       'ext.baz.fizzbuzz',
+                               ],
+                       ],
+                       1
+               );
+               $processor->extractInfo(
+                       $this->dir,
+                       [
+                               'name' => 'FooBar2',
+                               'FizzBuzzMorePlugins' => [
+                                       'ext.bar.fizzbuzz',
+                               ]
+                       ],
+                       1
+               );
+
+               $info = $processor->getExtractedInfo();
+               $this->assertArrayHasKey( 'FooBarPlugins', $info['attributes'] );
+               $this->assertSame( [ 'ext.baz.foobar' ], $info['attributes']['FooBarPlugins'] );
+               $this->assertArrayHasKey( 'FizzBuzzMorePlugins', $info['attributes'] );
+               $this->assertSame(
+                       [ 'ext.baz.fizzbuzz', 'ext.bar.fizzbuzz' ],
+                       $info['attributes']['FizzBuzzMorePlugins']
+               );
+       }
+
+       public function testAttributes1_notarray() {
+               $processor = new ExtensionProcessor();
+               $this->setExpectedException(
+                       InvalidArgumentException::class,
+                       "The value for 'FooBarPlugins' should be an array (from {$this->dir})"
+               );
+               $processor->extractInfo(
+                       $this->dir,
+                       [
+                               'FooBarPlugins' => 'ext.baz.foobar',
+                       ] + self::$default,
+                       1
+               );
+       }
+
+       public function testExtractPathBasedGlobal() {
+               $processor = new ExtensionProcessor();
+               $processor->extractInfo(
+                       $this->dir,
+                       [
+                               'ParserTestFiles' => [
+                                       'tests/parserTests.txt',
+                                       'tests/extraParserTests.txt',
+                               ],
+                               'ServiceWiringFiles' => [
+                                       'includes/ServiceWiring.php'
+                               ],
+                       ] + self::$default,
+                       1
+               );
+               $globals = $processor->getExtractedInfo()['globals'];
+               $this->assertArrayHasKey( 'wgParserTestFiles', $globals );
+               $this->assertSame( [
+                       "{$this->dirname}/tests/parserTests.txt",
+                       "{$this->dirname}/tests/extraParserTests.txt"
+               ], $globals['wgParserTestFiles'] );
+               $this->assertArrayHasKey( 'wgServiceWiringFiles', $globals );
+               $this->assertSame( [
+                       "{$this->dirname}/includes/ServiceWiring.php"
+               ], $globals['wgServiceWiringFiles'] );
+       }
+
+       public function testGetRequirements() {
+               $info = self::$default + [
+                       'requires' => [
+                               'MediaWiki' => '>= 1.25.0',
+                               'platform' => [
+                                       'php' => '>= 5.5.9'
+                               ],
+                               'extensions' => [
+                                       'Bar' => '*'
+                               ]
+                       ]
+               ];
+               $processor = new ExtensionProcessor();
+               $this->assertSame(
+                       $info['requires'],
+                       $processor->getRequirements( $info, false )
+               );
+               $this->assertSame(
+                       [],
+                       $processor->getRequirements( [], false )
+               );
+       }
+
+       public function testGetDevRequirements() {
+               $info = self::$default + [
+                       'dev-requires' => [
+                               'MediaWiki' => '>= 1.31.0',
+                               'platform' => [
+                                       'ext-foo' => '*',
+                               ],
+                               'skins' => [
+                                       'Baz' => '*',
+                               ],
+                               'extensions' => [
+                                       'Biz' => '*',
+                               ],
+                       ],
+               ];
+               $processor = new ExtensionProcessor();
+               $this->assertSame(
+                       $info['dev-requires'],
+                       $processor->getRequirements( $info, true )
+               );
+               // Set some standard requirements, so we can test merging
+               $info['requires'] = [
+                       'MediaWiki' => '>= 1.25.0',
+                       'platform' => [
+                               'php' => '>= 5.5.9'
+                       ],
+                       'extensions' => [
+                               'Bar' => '*'
+                       ]
+               ];
+               $this->assertSame(
+                       [
+                               'MediaWiki' => '>= 1.25.0 >= 1.31.0',
+                               'platform' => [
+                                       'php' => '>= 5.5.9',
+                                       'ext-foo' => '*',
+                               ],
+                               'extensions' => [
+                                       'Bar' => '*',
+                                       'Biz' => '*',
+                               ],
+                               'skins' => [
+                                       'Baz' => '*',
+                               ],
+                       ],
+                       $processor->getRequirements( $info, true )
+               );
+
+               // If there's no dev-requires, it just returns requires
+               unset( $info['dev-requires'] );
+               $this->assertSame(
+                       $info['requires'],
+                       $processor->getRequirements( $info, true )
+               );
+       }
+
+       public function testGetExtraAutoloaderPaths() {
+               $processor = new ExtensionProcessor();
+               $this->assertSame(
+                       [ "{$this->dirname}/vendor/autoload.php" ],
+                       $processor->getExtraAutoloaderPaths( $this->dirname, [
+                               'load_composer_autoloader' => true,
+                       ] )
+               );
+       }
+
+       /**
+        * Verify that extension.schema.json is in sync with ExtensionProcessor
+        *
+        * @coversNothing
+        */
+       public function testGlobalSettingsDocumentedInSchema() {
+               global $IP;
+               $globalSettings = TestingAccessWrapper::newFromClass(
+                       ExtensionProcessor::class )->globalSettings;
+
+               $version = ExtensionRegistry::MANIFEST_VERSION;
+               $schema = FormatJson::decode(
+                       file_get_contents( "$IP/docs/extension.schema.v$version.json" ),
+                       true
+               );
+               $missing = [];
+               foreach ( $globalSettings as $global ) {
+                       if ( !isset( $schema['properties'][$global] ) ) {
+                               $missing[] = $global;
+                       }
+               }
+
+               $this->assertEquals( [], $missing,
+                       "The following global settings are not documented in docs/extension.schema.json" );
+       }
+}
+
+/**
+ * Allow overriding the default value of $this->globals
+ * so we can test merging
+ */
+class MockExtensionProcessor extends ExtensionProcessor {
+       public function __construct( $globals = [] ) {
+               $this->globals = $globals + $this->globals;
+       }
+}
diff --git a/tests/phpunit/unit/includes/registration/VersionCheckerTest.php b/tests/phpunit/unit/includes/registration/VersionCheckerTest.php
new file mode 100644 (file)
index 0000000..e824e3f
--- /dev/null
@@ -0,0 +1,479 @@
+<?php
+
+/**
+ * @covers VersionChecker
+ */
+class VersionCheckerTest extends PHPUnit\Framework\TestCase {
+
+       use MediaWikiCoversValidator;
+       use PHPUnit4And6Compat;
+
+       /**
+        * @dataProvider provideMediaWikiCheck
+        */
+       public function testMediaWikiCheck( $coreVersion, $constraint, $expected ) {
+               $checker = new VersionChecker( $coreVersion, '7.0.0', [] );
+               $this->assertEquals( $expected, !(bool)$checker->checkArray( [
+                       'FakeExtension' => [
+                               'MediaWiki' => $constraint,
+                       ],
+               ] ) );
+       }
+
+       public static function provideMediaWikiCheck() {
+               return [
+                       // [ $wgVersion, constraint, expected ]
+                       [ '1.25alpha', '>= 1.26', false ],
+                       [ '1.25.0', '>= 1.26', false ],
+                       [ '1.26alpha', '>= 1.26', true ],
+                       [ '1.26alpha', '>= 1.26.0', true ],
+                       [ '1.26alpha', '>= 1.26.0-stable', false ],
+                       [ '1.26.0', '>= 1.26.0-stable', true ],
+                       [ '1.26.1', '>= 1.26.0-stable', true ],
+                       [ '1.27.1', '>= 1.26.0-stable', true ],
+                       [ '1.26alpha', '>= 1.26.1', false ],
+                       [ '1.26alpha', '>= 1.26alpha', true ],
+                       [ '1.26alpha', '>= 1.25', true ],
+                       [ '1.26.0-alpha.14', '>= 1.26.0-alpha.15', false ],
+                       [ '1.26.0-alpha.14', '>= 1.26.0-alpha.10', true ],
+                       [ '1.26.1', '>= 1.26.2, <=1.26.0', false ],
+                       [ '1.26.1', '^1.26.2', false ],
+                       // Accept anything for un-parsable version strings
+                       [ '1.26mwf14', '== 1.25alpha', true ],
+                       [ 'totallyinvalid', '== 1.0', true ],
+               ];
+       }
+
+       /**
+        * @dataProvider providePhpValidCheck
+        */
+       public function testPhpValidCheck( $phpVersion, $constraint, $expected ) {
+               $checker = new VersionChecker( '1.0.0', $phpVersion, [] );
+               $this->assertEquals( $expected, !(bool)$checker->checkArray( [
+                       'FakeExtension' => [
+                               'platform' => [
+                                       'php' => $constraint,
+                               ],
+                       ],
+               ] ) );
+       }
+
+       public static function providePhpValidCheck() {
+               return [
+                       // [ phpVersion, constraint, expected ]
+                       [ '7.0.23', '>= 7.0.0', true ],
+                       [ '7.0.23', '^7.1.0', false ],
+                       [ '7.0.23', '7.0.23', true ],
+               ];
+       }
+
+       /**
+        * @expectedException UnexpectedValueException
+        */
+       public function testPhpInvalidConstraint() {
+               $checker = new VersionChecker( '1.0.0', '7.0.0', [] );
+               $checker->checkArray( [
+                       'FakeExtension' => [
+                               'platform' => [
+                                       'php' => 'totallyinvalid',
+                               ],
+                       ],
+               ] );
+       }
+
+       /**
+        * @dataProvider providePhpInvalidVersion
+        * @expectedException UnexpectedValueException
+        */
+       public function testPhpInvalidVersion( $phpVersion ) {
+                $checker = new VersionChecker( '1.0.0', $phpVersion, [] );
+       }
+
+       public static function providePhpInvalidVersion() {
+               return [
+                       // [ phpVersion ]
+                       [ '7.abc' ],
+                       [ '5.a.x' ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideType
+        */
+       public function testType( $given, $expected ) {
+               $checker = new VersionChecker(
+                       '1.0.0',
+                       '7.0.0',
+                       [ 'phpLoadedExtension' ],
+                       [
+                               'presentAbility' => true,
+                               'presentAbilityWithMessage' => true,
+                               'missingAbility' => false,
+                               'missingAbilityWithMessage' => false,
+                       ],
+                       [
+                               'presentAbilityWithMessage' => 'Present.',
+                               'missingAbilityWithMessage' => 'Missing.',
+                       ]
+               );
+               $checker->setLoadedExtensionsAndSkins( [
+                               'FakeDependency' => [
+                                       'version' => '1.0.0',
+                               ],
+                               'NoVersionGiven' => [],
+                       ] );
+               $this->assertEquals( $expected, $checker->checkArray( [
+                       'FakeExtension' => $given,
+               ] ) );
+       }
+
+       public static function provideType() {
+               return [
+                       // valid type
+                       [
+                               [
+                                       'extensions' => [
+                                               'FakeDependency' => '1.0.0',
+                                       ],
+                               ],
+                               [],
+                       ],
+                       [
+                               [
+                                       'MediaWiki' => '1.0.0',
+                               ],
+                               [],
+                       ],
+                       [
+                               [
+                                       'extensions' => [
+                                               'NoVersionGiven' => '*',
+                                       ],
+                               ],
+                               [],
+                       ],
+                       [
+                               [
+                                       'extensions' => [
+                                               'NoVersionGiven' => '1.0',
+                                       ],
+                               ],
+                               [
+                                       [
+                                               'incompatible' => 'FakeExtension',
+                                               'type' => 'incompatible-extensions',
+                                               'msg' => 'NoVersionGiven does not expose its version, but FakeExtension requires: 1.0.',
+                                       ],
+                               ],
+                       ],
+                       [
+                               [
+                                       'extensions' => [
+                                               'Missing' => '*',
+                                       ],
+                               ],
+                               [
+                                       [
+                                               'missing' => 'Missing',
+                                               'type' => 'missing-extensions',
+                                               'msg' => 'FakeExtension requires Missing to be installed.',
+                                       ],
+                               ],
+                       ],
+                       [
+                               [
+                                       'extensions' => [
+                                               'FakeDependency' => '2.0.0',
+                                       ],
+                               ],
+                               [
+                                       [
+                                               'incompatible' => 'FakeExtension',
+                                               'type' => 'incompatible-extensions',
+                                               // phpcs:ignore Generic.Files.LineLength.TooLong
+                                               'msg' => 'FakeExtension is not compatible with the current installed version of FakeDependency (1.0.0), it requires: 2.0.0.',
+                                       ],
+                               ],
+                       ],
+                       [
+                               [
+                                       'skins' => [
+                                               'FakeSkin' => '*',
+                                       ],
+                               ],
+                               [
+                                       [
+                                               'missing' => 'FakeSkin',
+                                               'type' => 'missing-skins',
+                                               'msg' => 'FakeExtension requires FakeSkin to be installed.',
+                                       ],
+                               ],
+                       ],
+                       [
+                               [
+                                       'platform' => [
+                                               'ext-phpLoadedExtension' => '*',
+                                       ],
+                               ],
+                               [],
+                       ],
+                       [
+                               [
+                                       'platform' => [
+                                               'ext-phpMissingExtension' => '*',
+                                       ],
+                               ],
+                               [
+                                       [
+                                               'missing' => 'phpMissingExtension',
+                                               'type' => 'missing-phpExtension',
+                                               // phpcs:ignore Generic.Files.LineLength.TooLong
+                                               'msg' => 'FakeExtension requires phpMissingExtension PHP extension to be installed.',
+                                       ],
+                               ],
+                       ],
+                       [
+                               [
+                                       'platform' => [
+                                               'ability-presentAbility' => true,
+                                       ],
+                               ],
+                               [],
+                       ],
+                       [
+                               [
+                                       'platform' => [
+                                               'ability-presentAbilityWithMessage' => true,
+                                       ],
+                               ],
+                               [],
+                       ],
+                       [
+                               [
+                                       'platform' => [
+                                               'ability-presentAbility' => false,
+                                       ],
+                               ],
+                               [],
+                       ],
+                       [
+                               [
+                                       'platform' => [
+                                               'ability-presentAbilityWithMessage' => false,
+                                       ],
+                               ],
+                               [],
+                       ],
+                       [
+                               [
+                                       'platform' => [
+                                               'ability-missingAbility' => true,
+                                       ],
+                               ],
+                               [
+                                       [
+                                               'missing' => 'missingAbility',
+                                               'type' => 'missing-ability',
+                                               'msg' => 'FakeExtension requires "missingAbility" ability',
+                                       ],
+                               ],
+                       ],
+                       [
+                               [
+                                       'platform' => [
+                                               'ability-missingAbilityWithMessage' => true,
+                                       ],
+                               ],
+                               [
+                                       [
+                                               'missing' => 'missingAbilityWithMessage',
+                                               'type' => 'missing-ability',
+                                               // phpcs:ignore Generic.Files.LineLength.TooLong
+                                               'msg' => 'FakeExtension requires "missingAbilityWithMessage" ability: Missing.',
+                                       ],
+                               ],
+                       ],
+                       [
+                               [
+                                       'platform' => [
+                                               'ability-missingAbility' => false,
+                                       ],
+                               ],
+                               [],
+                       ],
+                       [
+                               [
+                                       'platform' => [
+                                               'ability-missingAbilityWithMessage' => false,
+                                       ],
+                               ],
+                               [],
+                       ],
+               ];
+       }
+
+       /**
+        * Check, if a non-parsable version constraint does not throw an exception or
+        * returns any error message.
+        */
+       public function testInvalidConstraint() {
+               $checker = new VersionChecker( '1.0.0', '7.0.0', [] );
+               $checker->setLoadedExtensionsAndSkins( [
+                               'FakeDependency' => [
+                                       'version' => 'not really valid',
+                               ],
+                       ] );
+               $this->assertEquals( [
+                       [
+                               'type' => 'invalid-version',
+                               'msg' => "FakeDependency does not have a valid version string.",
+                       ],
+               ], $checker->checkArray( [
+                       'FakeExtension' => [
+                               'extensions' => [
+                                       'FakeDependency' => '1.24.3',
+                               ],
+                       ],
+               ] ) );
+
+               $checker = new VersionChecker( '1.0.0', '7.0.0', [] );
+               $checker->setLoadedExtensionsAndSkins( [
+                               'FakeDependency' => [
+                                       'version' => '1.24.3',
+                               ],
+                       ] );
+
+               $this->setExpectedException( UnexpectedValueException::class );
+               $checker->checkArray( [
+                       'FakeExtension' => [
+                               'FakeDependency' => 'not really valid',
+                       ],
+               ] );
+       }
+
+       public function provideInvalidDependency() {
+               return [
+                       [
+                               [
+                                       'FakeExtension' => [
+                                               'platform' => [
+                                                       'undefinedPlatformDependency' => '*',
+                                               ],
+                                       ],
+                               ],
+                               'undefinedPlatformDependency',
+                       ],
+                       [
+                               [
+                                       'FakeExtension' => [
+                                               'platform' => [
+                                                       'phpLoadedExtension' => '*',
+                                               ],
+                                       ],
+                               ],
+                               'phpLoadedExtension',
+                       ],
+                       [
+                               [
+                                       'FakeExtension' => [
+                                               'platform' => [
+                                                       'ability-invalidAbility' => true,
+                                               ],
+                                       ],
+                               ],
+                               'ability-invalidAbility',
+                       ],
+                       [
+                               [
+                                       'FakeExtension' => [
+                                               'platform' => [
+                                                       'presentAbility' => true,
+                                               ],
+                                       ],
+                               ],
+                               'presentAbility',
+                       ],
+                       [
+                               [
+                                       'FakeExtension' => [
+                                               'undefinedDependencyType' => '*',
+                                       ],
+                               ],
+                               'undefinedDependencyType',
+                       ],
+                       // T197478
+                       [
+                               [
+                                       'FakeExtension' => [
+                                               'skin' => [
+                                                       'FakeSkin' => '*',
+                                               ],
+                                       ],
+                               ],
+                               'skin',
+                       ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideInvalidDependency
+        */
+       public function testInvalidDependency( $depencency, $type ) {
+               $checker = new VersionChecker(
+                       '1.0.0',
+                       '7.0.0',
+                       [ 'phpLoadedExtension' ],
+                       [
+                               'presentAbility' => true,
+                               'missingAbility' => false,
+                       ]
+               );
+               $this->setExpectedException(
+                       UnexpectedValueException::class,
+                       "Dependency type $type unknown in FakeExtension"
+               );
+               $checker->checkArray( $depencency );
+       }
+
+       public function testInvalidPhpExtensionConstraint() {
+               $checker = new VersionChecker( '1.0.0', '7.0.0', [ 'phpLoadedExtension' ] );
+               $this->setExpectedException(
+                       UnexpectedValueException::class,
+                       'Version constraints for PHP extensions are not supported in FakeExtension'
+               );
+               $checker->checkArray( [
+                       'FakeExtension' => [
+                               'platform' => [
+                                       'ext-phpLoadedExtension' => '1.0.0',
+                               ],
+                       ],
+               ] );
+       }
+
+       /**
+        * @dataProvider provideInvalidAbilityType
+        */
+       public function testInvalidAbilityType( $value ) {
+               $checker = new VersionChecker( '1.0.0', '7.0.0', [], [ 'presentAbility' => true ] );
+               $this->setExpectedException(
+                       UnexpectedValueException::class,
+                       'Only booleans are allowed to to indicate the presence of abilities in FakeExtension'
+               );
+               $checker->checkArray( [
+                       'FakeExtension' => [
+                               'platform' => [
+                                       'ability-presentAbility' => $value,
+                               ],
+                       ],
+               ] );
+       }
+
+       public function provideInvalidAbilityType() {
+               return [
+                       [ null ],
+                       [ 1 ],
+                       [ '1' ],
+               ];
+       }
+
+}
diff --git a/tests/phpunit/unit/includes/resourceloader/DerivativeResourceLoaderContextTest.php b/tests/phpunit/unit/includes/resourceloader/DerivativeResourceLoaderContextTest.php
new file mode 100644 (file)
index 0000000..e178e96
--- /dev/null
@@ -0,0 +1,138 @@
+<?php
+
+/**
+ * @group ResourceLoader
+ * @covers DerivativeResourceLoaderContext
+ */
+class DerivativeResourceLoaderContextTest extends PHPUnit\Framework\TestCase {
+
+       use MediaWikiCoversValidator;
+
+       protected static function makeContext() {
+               $request = new FauxRequest( [
+                               'lang' => 'qqx',
+                               'modules' => 'test.default',
+                               'only' => 'scripts',
+                               'skin' => 'fallback',
+                               'target' => 'test',
+               ] );
+               return new ResourceLoaderContext(
+                       new ResourceLoader( ResourceLoaderTestCase::getMinimalConfig() ),
+                       $request
+               );
+       }
+
+       public function testChangeModules() {
+               $derived = new DerivativeResourceLoaderContext( self::makeContext() );
+               $this->assertSame( $derived->getModules(), [ 'test.default' ], 'inherit from parent' );
+
+               $derived->setModules( [ 'test.override' ] );
+               $this->assertSame( $derived->getModules(), [ 'test.override' ] );
+       }
+
+       public function testChangeLanguageAndDirection() {
+               $derived = new DerivativeResourceLoaderContext( self::makeContext() );
+               $this->assertSame( $derived->getLanguage(), 'qqx', 'inherit from parent' );
+               $this->assertSame( $derived->getDirection(), 'ltr', 'inherit from parent' );
+
+               $derived->setLanguage( 'nl' );
+               $this->assertSame( $derived->getLanguage(), 'nl' );
+               $this->assertSame( $derived->getDirection(), 'ltr' );
+
+               // Changing the language must clear cache of computed direction
+               $derived->setLanguage( 'he' );
+               $this->assertSame( $derived->getDirection(), 'rtl' );
+               $this->assertSame( $derived->getLanguage(), 'he' );
+
+               // Overriding the direction explicitly is allowed
+               $derived->setDirection( 'ltr' );
+               $this->assertSame( $derived->getDirection(), 'ltr' );
+               $this->assertSame( $derived->getLanguage(), 'he' );
+       }
+
+       public function testChangeSkin() {
+               $derived = new DerivativeResourceLoaderContext( self::makeContext() );
+               $this->assertSame( $derived->getSkin(), 'fallback', 'inherit from parent' );
+
+               $derived->setSkin( 'myskin' );
+               $this->assertSame( $derived->getSkin(), 'myskin' );
+       }
+
+       public function testChangeUser() {
+               $derived = new DerivativeResourceLoaderContext( self::makeContext() );
+               $this->assertSame( $derived->getUser(), null, 'inherit from parent' );
+
+               $derived->setUser( 'MyUser' );
+               $this->assertSame( $derived->getUser(), 'MyUser' );
+       }
+
+       public function testChangeDebug() {
+               $derived = new DerivativeResourceLoaderContext( self::makeContext() );
+               $this->assertSame( $derived->getDebug(), false, 'inherit from parent' );
+
+               $derived->setDebug( true );
+               $this->assertSame( $derived->getDebug(), true );
+       }
+
+       public function testChangeOnly() {
+               $derived = new DerivativeResourceLoaderContext( self::makeContext() );
+               $this->assertSame( $derived->getOnly(), 'scripts', 'inherit from parent' );
+
+               $derived->setOnly( 'styles' );
+               $this->assertSame( $derived->getOnly(), 'styles' );
+
+               $derived->setOnly( null );
+               $this->assertSame( $derived->getOnly(), null );
+       }
+
+       public function testChangeVersion() {
+               $derived = new DerivativeResourceLoaderContext( self::makeContext() );
+               $this->assertSame( $derived->getVersion(), null );
+
+               $derived->setVersion( 'hw1' );
+               $this->assertSame( $derived->getVersion(), 'hw1' );
+       }
+
+       public function testChangeRaw() {
+               $derived = new DerivativeResourceLoaderContext( self::makeContext() );
+               $this->assertSame( $derived->getRaw(), false, 'inherit from parent' );
+
+               $derived->setRaw( true );
+               $this->assertSame( $derived->getRaw(), true );
+       }
+
+       public function testChangeHash() {
+               $derived = new DerivativeResourceLoaderContext( self::makeContext() );
+               $this->assertSame( $derived->getHash(), 'qqx|fallback|||scripts|||||', 'inherit' );
+
+               $derived->setLanguage( 'nl' );
+               $derived->setUser( 'Example' );
+               // Assert that subclass is able to clear parent class "hash" member
+               $this->assertSame( $derived->getHash(), 'nl|fallback||Example|scripts|||||' );
+       }
+
+       public function testChangeContentOverrides() {
+               $derived = new DerivativeResourceLoaderContext( self::makeContext() );
+               $this->assertNull( $derived->getContentOverrideCallback(), 'default' );
+
+               $override = function ( Title $t ) {
+                       return null;
+               };
+               $derived->setContentOverrideCallback( $override );
+               $this->assertSame( $override, $derived->getContentOverrideCallback(), 'changed' );
+
+               $derived2 = new DerivativeResourceLoaderContext( $derived );
+               $this->assertSame(
+                       $override,
+                       $derived2->getContentOverrideCallback(),
+                       'change via a second derivative layer'
+               );
+       }
+
+       public function testImmutableAccessors() {
+               $context = self::makeContext();
+               $derived = new DerivativeResourceLoaderContext( $context );
+               $this->assertSame( $derived->getRequest(), $context->getRequest() );
+               $this->assertSame( $derived->getResourceLoader(), $context->getResourceLoader() );
+       }
+}
diff --git a/tests/phpunit/unit/includes/resourceloader/MessageBlobStoreTest.php b/tests/phpunit/unit/includes/resourceloader/MessageBlobStoreTest.php
new file mode 100644 (file)
index 0000000..d8a94e7
--- /dev/null
@@ -0,0 +1,214 @@
+<?php
+
+use Wikimedia\Rdbms\Database;
+use Wikimedia\Rdbms\LoadBalancer;
+use Wikimedia\TestingAccessWrapper;
+
+/**
+ * @group ResourceLoader
+ * @covers MessageBlobStore
+ */
+class MessageBlobStoreTest extends MediaWikiUnitTestCase {
+
+       use MediaWikiCoversValidator;
+       use PHPUnit4And6Compat;
+
+       protected function setUp() {
+               parent::setUp();
+               // MediaWiki's test wrapper sets $wgMainWANCache to CACHE_NONE.
+               // Use HashBagOStuff here so that we can observe caching.
+               $this->wanCache = new WANObjectCache( [
+                       'cache' => new HashBagOStuff()
+               ] );
+
+               $this->clock = 1301655600.000;
+               $this->wanCache->setMockTime( $this->clock );
+
+               $lbMock = $this->createMock( LoadBalancer::class );
+               $dbMock = $this->getMockBuilder( Database::class )
+                       ->disableOriginalConstructor()
+                       ->getMockForAbstractClass();
+
+               $lbMock->expects( $this->any() )
+                       ->method( 'getConnection' )
+                       ->willReturn( $dbMock );
+
+               $lbMockFactory = function () use ( $lbMock ): LoadBalancer {
+                       return $lbMock;
+               };
+
+               $this->overrideMwServices( [ 'DBLoadBalancer' => $lbMockFactory ] );
+       }
+
+       public function testBlobCreation() {
+               $module = $this->makeModule( [ 'mainpage' ] );
+               $rl = new EmptyResourceLoader();
+               $rl->register( $module->getName(), $module );
+
+               $blobStore = $this->makeBlobStore( null, $rl );
+               $blob = $blobStore->getBlob( $module, 'en' );
+
+               $this->assertEquals( '{"mainpage":"Main Page"}', $blob, 'Generated blob' );
+       }
+
+       public function testBlobCreation_empty() {
+               $module = $this->makeModule( [] );
+               $rl = new EmptyResourceLoader();
+               $rl->register( $module->getName(), $module );
+
+               $blobStore = $this->makeBlobStore( null, $rl );
+               $blob = $blobStore->getBlob( $module, 'en' );
+
+               $this->assertEquals( '{}', $blob, 'Generated blob' );
+       }
+
+       public function testBlobCreation_unknownMessage() {
+               $module = $this->makeModule( [ 'i-dont-exist', 'mainpage', 'i-dont-exist2' ] );
+               $rl = new EmptyResourceLoader();
+               $rl->register( $module->getName(), $module );
+               $blobStore = $this->makeBlobStore( null, $rl );
+
+               // Generating a blob should continue without errors,
+               // with keys of unknown messages excluded from the blob.
+               $blob = $blobStore->getBlob( $module, 'en' );
+               $this->assertEquals( '{"mainpage":"Main Page"}', $blob, 'Generated blob' );
+       }
+
+       public function testMessageCachingAndPurging() {
+               $module = $this->makeModule( [ 'example' ] );
+               $rl = new EmptyResourceLoader();
+               $rl->register( $module->getName(), $module );
+               $blobStore = $this->makeBlobStore( [ 'fetchMessage' ], $rl );
+
+               // Advance this new WANObjectCache instance to a normal state,
+               // by doing one "get" and letting its hold off period expire.
+               // Without this, the first real "get" would lazy-initialise the
+               // checkKey and thus reject the first "set".
+               $blobStore->getBlob( $module, 'en' );
+               $this->clock += 20;
+
+               // Arrange version 1 of a message
+               $blobStore->expects( $this->once() )
+                       ->method( 'fetchMessage' )
+                       ->will( $this->returnValue( 'First version' ) );
+
+               // Assert
+               $blob = $blobStore->getBlob( $module, 'en' );
+               $this->assertEquals( '{"example":"First version"}', $blob, 'Blob for v1' );
+
+               // Arrange version 2
+               $blobStore = $this->makeBlobStore( [ 'fetchMessage' ], $rl );
+               $blobStore->expects( $this->once() )
+                       ->method( 'fetchMessage' )
+                       ->will( $this->returnValue( 'Second version' ) );
+               $this->clock += 20;
+
+               // Assert
+               // We do not validate whether a cached message is up-to-date.
+               // Instead, changes to messages will send us a purge.
+               // When cache is not purged or expired, it must be used.
+               $blob = $blobStore->getBlob( $module, 'en' );
+               $this->assertEquals( '{"example":"First version"}', $blob, 'Reuse cached v1 blob' );
+
+               // Purge cache
+               $blobStore->updateMessage( 'example' );
+               $this->clock += 20;
+
+               // Assert
+               $blob = $blobStore->getBlob( $module, 'en' );
+               $this->assertEquals( '{"example":"Second version"}', $blob, 'Updated blob for v2' );
+       }
+
+       public function testPurgeEverything() {
+               $module = $this->makeModule( [ 'example' ] );
+               $rl = new EmptyResourceLoader();
+               $rl->register( $module->getName(), $module );
+               $blobStore = $this->makeBlobStore( [ 'fetchMessage' ], $rl );
+               // Advance this new WANObjectCache instance to a normal state.
+               $blobStore->getBlob( $module, 'en' );
+               $this->clock += 20;
+
+               // Arrange version 1 and 2
+               $blobStore->expects( $this->exactly( 2 ) )
+                       ->method( 'fetchMessage' )
+                       ->will( $this->onConsecutiveCalls( 'First', 'Second' ) );
+
+               // Assert
+               $blob = $blobStore->getBlob( $module, 'en' );
+               $this->assertEquals( '{"example":"First"}', $blob, 'Blob for v1' );
+
+               $this->clock += 20;
+
+               // Assert
+               $blob = $blobStore->getBlob( $module, 'en' );
+               $this->assertEquals( '{"example":"First"}', $blob, 'Blob for v1 again' );
+
+               // Purge everything
+               $blobStore->clear();
+               $this->clock += 20;
+
+               // Assert
+               $blob = $blobStore->getBlob( $module, 'en' );
+               $this->assertEquals( '{"example":"Second"}', $blob, 'Blob for v2' );
+       }
+
+       public function testValidateAgainstModuleRegistry() {
+               // Arrange version 1 of a module
+               $module = $this->makeModule( [ 'foo' ] );
+               $rl = new EmptyResourceLoader();
+               $rl->register( $module->getName(), $module );
+               $blobStore = $this->makeBlobStore( [ 'fetchMessage' ], $rl );
+               $blobStore->expects( $this->once() )
+                       ->method( 'fetchMessage' )
+                       ->will( $this->returnValueMap( [
+                               // message key, language code, message value
+                               [ 'foo', 'en', 'Hello' ],
+                       ] ) );
+
+               // Assert
+               $blob = $blobStore->getBlob( $module, 'en' );
+               $this->assertEquals( '{"foo":"Hello"}', $blob, 'Blob for v1' );
+
+               // Arrange version 2 of module
+               // While message values may be out of date, the set of messages returned
+               // must always match the set of message keys required by the module.
+               // We do not receive purges for this because no messages were changed.
+               $module = $this->makeModule( [ 'foo', 'bar' ] );
+               $rl = new EmptyResourceLoader();
+               $rl->register( $module->getName(), $module );
+               $blobStore = $this->makeBlobStore( [ 'fetchMessage' ], $rl );
+               $blobStore->expects( $this->exactly( 2 ) )
+                       ->method( 'fetchMessage' )
+                       ->will( $this->returnValueMap( [
+                               // message key, language code, message value
+                               [ 'foo', 'en', 'Hello' ],
+                               [ 'bar', 'en', 'World' ],
+                       ] ) );
+
+               // Assert
+               $blob = $blobStore->getBlob( $module, 'en' );
+               $this->assertEquals( '{"foo":"Hello","bar":"World"}', $blob, 'Blob for v2' );
+       }
+
+       public function testSetLoggedIsVoid() {
+               $blobStore = $this->makeBlobStore();
+               $this->assertSame( null, $blobStore->setLogger( new Psr\Log\NullLogger() ) );
+       }
+
+       private function makeBlobStore( $methods = null, $rl = null ) {
+               $blobStore = $this->getMockBuilder( MessageBlobStore::class )
+                       ->setConstructorArgs( [ $rl ?? $this->createMock( ResourceLoader::class ) ] )
+                       ->setMethods( $methods )
+                       ->getMock();
+
+               $access = TestingAccessWrapper::newFromObject( $blobStore );
+               $access->wanCache = $this->wanCache;
+               return $blobStore;
+       }
+
+       private function makeModule( array $messages ) {
+               $module = new ResourceLoaderTestModule( [ 'messages' => $messages ] );
+               $module->setName( 'test.blobstore' );
+               return $module;
+       }
+}
diff --git a/tests/phpunit/unit/includes/resourceloader/ResourceLoaderClientHtmlTest.php b/tests/phpunit/unit/includes/resourceloader/ResourceLoaderClientHtmlTest.php
new file mode 100644 (file)
index 0000000..03a3e24
--- /dev/null
@@ -0,0 +1,434 @@
+<?php
+
+use Wikimedia\TestingAccessWrapper;
+
+/**
+ * @group ResourceLoader
+ * @covers ResourceLoaderClientHtml
+ */
+class ResourceLoaderClientHtmlTest extends PHPUnit\Framework\TestCase {
+
+       use MediaWikiCoversValidator;
+
+       public function testGetData() {
+               $context = self::makeContext();
+               $context->getResourceLoader()->register( self::makeSampleModules() );
+
+               $client = new ResourceLoaderClientHtml( $context );
+               $client->setModules( [
+                       'test',
+                       'test.private',
+                       'test.shouldembed.empty',
+                       'test.shouldembed',
+                       'test.user',
+                       'test.unregistered',
+               ] );
+               $client->setModuleStyles( [
+                       'test.styles.mixed',
+                       'test.styles.user.empty',
+                       'test.styles.private',
+                       'test.styles.pure',
+                       'test.styles.shouldembed',
+                       'test.styles.deprecated',
+                       'test.unregistered.styles',
+               ] );
+
+               $expected = [
+                       'states' => [
+                               // The below are NOT queued for loading via `mw.loader.load(Array)`.
+                               // Instead we tell the client to set their state to "loading" so that
+                               // if they are needed as dependencies, the client will not try to
+                               // load them on-demand, because the server is taking care of them already.
+                               // Either:
+                               // - Embedded as inline scripts in the HTML (e.g. user-private code, and
+                               //   previews). Once that script tag is reached, the state is "loaded".
+                               // - Loaded directly from the HTML with a dedicated HTTP request (e.g.
+                               //   user scripts, which vary by a 'user' and 'version' parameter that
+                               //   the static user-agnostic startup module won't have).
+                               'test.private' => 'loading',
+                               'test.shouldembed' => 'loading',
+                               'test.user' => 'loading',
+                               // The below are known to the server to be empty scripts, or to be
+                               // synchronously loaded stylesheets. These start in the "ready" state.
+                               'test.shouldembed.empty' => 'ready',
+                               'test.styles.pure' => 'ready',
+                               'test.styles.user.empty' => 'ready',
+                               'test.styles.private' => 'ready',
+                               'test.styles.shouldembed' => 'ready',
+                               'test.styles.deprecated' => 'ready',
+                       ],
+                       'general' => [
+                               'test',
+                       ],
+                       'styles' => [
+                               'test.styles.pure',
+                               'test.styles.deprecated',
+                       ],
+                       'embed' => [
+                               'styles' => [ 'test.styles.private', 'test.styles.shouldembed' ],
+                               'general' => [
+                                       'test.private',
+                                       'test.shouldembed',
+                                       'test.user',
+                               ],
+                       ],
+                       'styleDeprecations' => [
+                               Xml::encodeJsCall(
+                                       'mw.log.warn',
+                                       [ 'This page is using the deprecated ResourceLoader module "test.styles.deprecated".
+Deprecation message.' ]
+                               )
+                       ],
+               ];
+
+               $access = TestingAccessWrapper::newFromObject( $client );
+               $this->assertEquals( $expected, $access->getData() );
+       }
+
+       public function testGetHeadHtml() {
+               $context = self::makeContext();
+               $context->getResourceLoader()->register( self::makeSampleModules() );
+
+               $client = new ResourceLoaderClientHtml( $context, [
+                       'nonce' => false,
+               ] );
+               $client->setConfig( [ 'key' => 'value' ] );
+               $client->setModules( [
+                       'test',
+                       'test.private',
+               ] );
+               $client->setModuleStyles( [
+                       'test.styles.pure',
+                       'test.styles.private',
+                       'test.styles.deprecated',
+               ] );
+               $client->setExemptStates( [
+                       'test.exempt' => 'ready',
+               ] );
+
+               // phpcs:disable Generic.Files.LineLength
+               $expected = '<script>'
+                       . 'document.documentElement.className=document.documentElement.className.replace(/(^|\s)client-nojs(\s|$)/,"$1client-js$2");'
+                       . 'RLCONF={"key":"value"};'
+                       . 'RLSTATE={"test.exempt":"ready","test.private":"loading","test.styles.pure":"ready","test.styles.private":"ready","test.styles.deprecated":"ready"};'
+                       . 'RLPAGEMODULES=["test"];'
+                       . '</script>' . "\n"
+                       . '<script>(RLQ=window.RLQ||[]).push(function(){'
+                       . 'mw.loader.implement("test.private@{blankVer}",null,{"css":[]});'
+                       . '});</script>' . "\n"
+                       . '<link rel="stylesheet" href="/w/load.php?lang=nl&amp;modules=test.styles.deprecated%2Cpure&amp;only=styles"/>' . "\n"
+                       . '<style>.private{}</style>' . "\n"
+                       . '<script async="" src="/w/load.php?lang=nl&amp;modules=startup&amp;only=scripts"></script>';
+               // phpcs:enable
+               $expected = self::expandVariables( $expected );
+
+               $this->assertSame( $expected, (string)$client->getHeadHtml() );
+       }
+
+       /**
+        * Confirm that 'target' is passed down to the startup module's load url.
+        */
+       public function testGetHeadHtmlWithTarget() {
+               $client = new ResourceLoaderClientHtml(
+                       self::makeContext(),
+                       [ 'target' => 'example' ]
+               );
+
+               // phpcs:disable Generic.Files.LineLength
+               $expected = '<script>document.documentElement.className=document.documentElement.className.replace(/(^|\s)client-nojs(\s|$)/,"$1client-js$2");</script>' . "\n"
+                       . '<script async="" src="/w/load.php?lang=nl&amp;modules=startup&amp;only=scripts&amp;target=example"></script>';
+               // phpcs:enable
+
+               $this->assertSame( $expected, (string)$client->getHeadHtml() );
+       }
+
+       /**
+        * Confirm that 'safemode' is passed down to startup.
+        */
+       public function testGetHeadHtmlWithSafemode() {
+               $client = new ResourceLoaderClientHtml(
+                       self::makeContext(),
+                       [ 'safemode' => '1' ]
+               );
+
+               // phpcs:disable Generic.Files.LineLength
+               $expected = '<script>document.documentElement.className=document.documentElement.className.replace(/(^|\s)client-nojs(\s|$)/,"$1client-js$2");</script>' . "\n"
+                       . '<script async="" src="/w/load.php?lang=nl&amp;modules=startup&amp;only=scripts&amp;safemode=1"></script>';
+               // phpcs:enable
+
+               $this->assertSame( $expected, (string)$client->getHeadHtml() );
+       }
+
+       /**
+        * Confirm that a null 'target' is the same as no target.
+        */
+       public function testGetHeadHtmlWithNullTarget() {
+               $client = new ResourceLoaderClientHtml(
+                       self::makeContext(),
+                       [ 'target' => null ]
+               );
+
+               // phpcs:disable Generic.Files.LineLength
+               $expected = '<script>document.documentElement.className=document.documentElement.className.replace(/(^|\s)client-nojs(\s|$)/,"$1client-js$2");</script>' . "\n"
+                       . '<script async="" src="/w/load.php?lang=nl&amp;modules=startup&amp;only=scripts"></script>';
+               // phpcs:enable
+
+               $this->assertSame( $expected, (string)$client->getHeadHtml() );
+       }
+
+       public function testGetBodyHtml() {
+               $context = self::makeContext();
+               $context->getResourceLoader()->register( self::makeSampleModules() );
+
+               $client = new ResourceLoaderClientHtml( $context, [ 'nonce' => false ] );
+               $client->setConfig( [ 'key' => 'value' ] );
+               $client->setModules( [
+                       'test',
+                       'test.private.bottom',
+               ] );
+               $client->setModuleStyles( [
+                       'test.styles.deprecated',
+               ] );
+               // phpcs:disable Generic.Files.LineLength
+               $expected = '<script>(RLQ=window.RLQ||[]).push(function(){'
+                       . 'mw.log.warn("This page is using the deprecated ResourceLoader module \"test.styles.deprecated\".\nDeprecation message.");'
+                       . '});</script>';
+               // phpcs:enable
+
+               $this->assertSame( $expected, (string)$client->getBodyHtml() );
+       }
+
+       public static function provideMakeLoad() {
+               // phpcs:disable Generic.Files.LineLength
+               return [
+                       [
+                               'context' => [],
+                               'modules' => [ 'test.unknown' ],
+                               'only' => ResourceLoaderModule::TYPE_STYLES,
+                               'extra' => [],
+                               'output' => '',
+                       ],
+                       [
+                               'context' => [],
+                               'modules' => [ 'test.styles.private' ],
+                               'only' => ResourceLoaderModule::TYPE_STYLES,
+                               'extra' => [],
+                               'output' => '<style>.private{}</style>',
+                       ],
+                       [
+                               'context' => [],
+                               'modules' => [ 'test.private' ],
+                               'only' => ResourceLoaderModule::TYPE_COMBINED,
+                               'extra' => [],
+                               'output' => '<script>(RLQ=window.RLQ||[]).push(function(){mw.loader.implement("test.private@{blankVer}",null,{"css":[]});});</script>',
+                       ],
+                       [
+                               'context' => [],
+                               // Eg. startup module
+                               'modules' => [ 'test.scripts.raw' ],
+                               'only' => ResourceLoaderModule::TYPE_SCRIPTS,
+                               'extra' => [],
+                               'output' => '<script async="" src="/w/load.php?lang=nl&amp;modules=test.scripts.raw&amp;only=scripts"></script>',
+                       ],
+                       [
+                               'context' => [],
+                               'modules' => [ 'test.scripts.raw' ],
+                               'only' => ResourceLoaderModule::TYPE_SCRIPTS,
+                               'extra' => [ 'sync' => '1' ],
+                               'output' => '<script src="/w/load.php?lang=nl&amp;modules=test.scripts.raw&amp;only=scripts&amp;sync=1"></script>',
+                       ],
+                       [
+                               'context' => [],
+                               'modules' => [ 'test.scripts.user' ],
+                               'only' => ResourceLoaderModule::TYPE_SCRIPTS,
+                               'extra' => [],
+                               'output' => '<script>(RLQ=window.RLQ||[]).push(function(){mw.loader.load("/w/load.php?lang=nl\u0026modules=test.scripts.user\u0026only=scripts\u0026user=Example\u0026version=0a56zyi");});</script>',
+                       ],
+                       [
+                               'context' => [],
+                               'modules' => [ 'test.user' ],
+                               'only' => ResourceLoaderModule::TYPE_COMBINED,
+                               'extra' => [],
+                               'output' => '<script>(RLQ=window.RLQ||[]).push(function(){mw.loader.load("/w/load.php?lang=nl\u0026modules=test.user\u0026user=Example\u0026version=0a56zyi");});</script>',
+                       ],
+                       [
+                               'context' => [ 'debug' => 'true' ],
+                               'modules' => [ 'test.styles.pure', 'test.styles.mixed' ],
+                               'only' => ResourceLoaderModule::TYPE_STYLES,
+                               'extra' => [],
+                               'output' => '<link rel="stylesheet" href="/w/load.php?debug=true&amp;lang=nl&amp;modules=test.styles.mixed&amp;only=styles"/>' . "\n"
+                                       . '<link rel="stylesheet" href="/w/load.php?debug=true&amp;lang=nl&amp;modules=test.styles.pure&amp;only=styles"/>',
+                       ],
+                       [
+                               'context' => [ 'debug' => 'false' ],
+                               'modules' => [ 'test.styles.pure', 'test.styles.mixed' ],
+                               'only' => ResourceLoaderModule::TYPE_STYLES,
+                               'extra' => [],
+                               'output' => '<link rel="stylesheet" href="/w/load.php?lang=nl&amp;modules=test.styles.mixed%2Cpure&amp;only=styles"/>',
+                       ],
+                       [
+                               'context' => [],
+                               'modules' => [ 'test.styles.noscript' ],
+                               'only' => ResourceLoaderModule::TYPE_STYLES,
+                               'extra' => [],
+                               'output' => '<noscript><link rel="stylesheet" href="/w/load.php?lang=nl&amp;modules=test.styles.noscript&amp;only=styles"/></noscript>',
+                       ],
+                       [
+                               'context' => [],
+                               'modules' => [ 'test.shouldembed' ],
+                               'only' => ResourceLoaderModule::TYPE_COMBINED,
+                               'extra' => [],
+                               'output' => '<script>(RLQ=window.RLQ||[]).push(function(){mw.loader.implement("test.shouldembed@09p30q0",null,{"css":[]});});</script>',
+                       ],
+                       [
+                               'context' => [],
+                               'modules' => [ 'test.styles.shouldembed' ],
+                               'only' => ResourceLoaderModule::TYPE_STYLES,
+                               'extra' => [],
+                               'output' => '<style>.shouldembed{}</style>',
+                       ],
+                       [
+                               'context' => [],
+                               'modules' => [ 'test.scripts.shouldembed' ],
+                               'only' => ResourceLoaderModule::TYPE_SCRIPTS,
+                               'extra' => [],
+                               'output' => '<script>(RLQ=window.RLQ||[]).push(function(){mw.loader.state({"test.scripts.shouldembed":"ready"});});</script>',
+                       ],
+                       [
+                               'context' => [],
+                               'modules' => [ 'test', 'test.shouldembed' ],
+                               'only' => ResourceLoaderModule::TYPE_COMBINED,
+                               'extra' => [],
+                               'output' => '<script>(RLQ=window.RLQ||[]).push(function(){mw.loader.load("/w/load.php?lang=nl\u0026modules=test");mw.loader.implement("test.shouldembed@09p30q0",null,{"css":[]});});</script>',
+                       ],
+                       [
+                               'context' => [],
+                               'modules' => [ 'test.styles.pure', 'test.styles.shouldembed' ],
+                               'only' => ResourceLoaderModule::TYPE_STYLES,
+                               'extra' => [],
+                               'output' =>
+                                       '<link rel="stylesheet" href="/w/load.php?lang=nl&amp;modules=test.styles.pure&amp;only=styles"/>' . "\n"
+                                       . '<style>.shouldembed{}</style>'
+                       ],
+                       [
+                               'context' => [],
+                               'modules' => [ 'test.ordering.a', 'test.ordering.e', 'test.ordering.b', 'test.ordering.d', 'test.ordering.c' ],
+                               'only' => ResourceLoaderModule::TYPE_STYLES,
+                               'extra' => [],
+                               'output' =>
+                                       '<link rel="stylesheet" href="/w/load.php?lang=nl&amp;modules=test.ordering.a%2Cb&amp;only=styles"/>' . "\n"
+                                       . '<style>.orderingC{}.orderingD{}</style>' . "\n"
+                                       . '<link rel="stylesheet" href="/w/load.php?lang=nl&amp;modules=test.ordering.e&amp;only=styles"/>'
+                       ],
+               ];
+               // phpcs:enable
+       }
+
+       /**
+        * @dataProvider provideMakeLoad
+        * @covers ResourceLoaderClientHtml
+        * @covers ResourceLoaderModule::getModuleContent
+        * @covers ResourceLoader
+        */
+       public function testMakeLoad(
+               array $contextQuery,
+               array $modules,
+               $type,
+               array $extraQuery,
+               $expected
+       ) {
+               $context = self::makeContext( $contextQuery );
+               $context->getResourceLoader()->register( self::makeSampleModules() );
+               $actual = ResourceLoaderClientHtml::makeLoad( $context, $modules, $type, $extraQuery, false );
+               $expected = self::expandVariables( $expected );
+               $this->assertSame( $expected, (string)$actual );
+       }
+
+       public function testGetDocumentAttributes() {
+               $client = new ResourceLoaderClientHtml( self::makeContext() );
+               $this->assertInternalType( 'array', $client->getDocumentAttributes() );
+       }
+
+       private static function expandVariables( $text ) {
+               return strtr( $text, [
+                       '{blankVer}' => ResourceLoaderTestCase::BLANK_VERSION
+               ] );
+       }
+
+       private static function makeContext( $extraQuery = [] ) {
+               $conf = new HashConfig( [
+                       'ResourceModuleSkinStyles' => [],
+                       'ResourceModules' => [],
+                       'EnableJavaScriptTest' => false,
+                       'LoadScript' => '/w/load.php',
+               ] );
+               return new ResourceLoaderContext(
+                       new ResourceLoader( $conf ),
+                       new FauxRequest( array_merge( [
+                               'lang' => 'nl',
+                               'skin' => 'fallback',
+                               'user' => 'Example',
+                               'target' => 'phpunit',
+                       ], $extraQuery ) )
+               );
+       }
+
+       private static function makeModule( array $options = [] ) {
+               return new ResourceLoaderTestModule( $options );
+       }
+
+       private static function makeSampleModules() {
+               $modules = [
+                       'test' => [],
+                       'test.private' => [ 'group' => 'private' ],
+                       'test.shouldembed.empty' => [ 'shouldEmbed' => true, 'isKnownEmpty' => true ],
+                       'test.shouldembed' => [ 'shouldEmbed' => true ],
+                       'test.user' => [ 'group' => 'user' ],
+
+                       'test.styles.pure' => [ 'type' => ResourceLoaderModule::LOAD_STYLES ],
+                       'test.styles.mixed' => [],
+                       'test.styles.noscript' => [
+                               'type' => ResourceLoaderModule::LOAD_STYLES,
+                               'group' => 'noscript',
+                       ],
+                       'test.styles.user' => [
+                               'type' => ResourceLoaderModule::LOAD_STYLES,
+                               'group' => 'user',
+                       ],
+                       'test.styles.user.empty' => [
+                               'type' => ResourceLoaderModule::LOAD_STYLES,
+                               'group' => 'user',
+                               'isKnownEmpty' => true,
+                       ],
+                       'test.styles.private' => [
+                               'type' => ResourceLoaderModule::LOAD_STYLES,
+                               'group' => 'private',
+                               'styles' => '.private{}',
+                       ],
+                       'test.styles.shouldembed' => [
+                               'type' => ResourceLoaderModule::LOAD_STYLES,
+                               'shouldEmbed' => true,
+                               'styles' => '.shouldembed{}',
+                       ],
+                       'test.styles.deprecated' => [
+                               'type' => ResourceLoaderModule::LOAD_STYLES,
+                               'deprecated' => 'Deprecation message.',
+                       ],
+
+                       'test.scripts' => [],
+                       'test.scripts.user' => [ 'group' => 'user' ],
+                       'test.scripts.user.empty' => [ 'group' => 'user', 'isKnownEmpty' => true ],
+                       'test.scripts.raw' => [ 'isRaw' => true ],
+                       'test.scripts.shouldembed' => [ 'shouldEmbed' => true ],
+
+                       'test.ordering.a' => [ 'shouldEmbed' => false ],
+                       'test.ordering.b' => [ 'shouldEmbed' => false ],
+                       'test.ordering.c' => [ 'shouldEmbed' => true, 'styles' => '.orderingC{}' ],
+                       'test.ordering.d' => [ 'shouldEmbed' => true, 'styles' => '.orderingD{}' ],
+                       'test.ordering.e' => [ 'shouldEmbed' => false ],
+               ];
+               return array_map( function ( $options ) {
+                       return self::makeModule( $options );
+               }, $modules );
+       }
+}
diff --git a/tests/phpunit/unit/includes/resourceloader/ResourceLoaderContextTest.php b/tests/phpunit/unit/includes/resourceloader/ResourceLoaderContextTest.php
new file mode 100644 (file)
index 0000000..2ec8ea9
--- /dev/null
@@ -0,0 +1,122 @@
+<?php
+
+/**
+ * See also:
+ * - ResourceLoaderImageModuleTest::testContext
+ *
+ * @group ResourceLoader
+ * @covers ResourceLoaderContext
+ */
+class ResourceLoaderContextTest extends PHPUnit\Framework\TestCase {
+
+       use MediaWikiCoversValidator;
+
+       protected static function getResourceLoader() {
+               return new EmptyResourceLoader( new HashConfig( [
+                       'ResourceLoaderDebug' => false,
+                       'LoadScript' => '/w/load.php',
+                       // For ResourceLoader::register()
+                       'ResourceModuleSkinStyles' => [],
+               ] ) );
+       }
+
+       public function testEmpty() {
+               $ctx = new ResourceLoaderContext( $this->getResourceLoader(), new FauxRequest( [] ) );
+
+               // Request parameters
+               $this->assertEquals( [], $ctx->getModules() );
+               $this->assertEquals( 'qqx', $ctx->getLanguage() );
+               $this->assertEquals( false, $ctx->getDebug() );
+               $this->assertEquals( null, $ctx->getOnly() );
+               $this->assertEquals( 'fallback', $ctx->getSkin() );
+               $this->assertEquals( null, $ctx->getUser() );
+               $this->assertNull( $ctx->getContentOverrideCallback() );
+
+               // Misc
+               $this->assertEquals( 'ltr', $ctx->getDirection() );
+               $this->assertEquals( 'qqx|fallback||||||||', $ctx->getHash() );
+               $this->assertInstanceOf( User::class, $ctx->getUserObj() );
+       }
+
+       public function testDummy() {
+               $this->assertInstanceOf(
+                       ResourceLoaderContext::class,
+                       ResourceLoaderContext::newDummyContext()
+               );
+       }
+
+       public function testAccessors() {
+               $ctx = new ResourceLoaderContext( $this->getResourceLoader(), new FauxRequest( [] ) );
+               $this->assertInstanceOf( ResourceLoader::class, $ctx->getResourceLoader() );
+               $this->assertInstanceOf( Config::class, $ctx->getConfig() );
+               $this->assertInstanceOf( WebRequest::class, $ctx->getRequest() );
+               $this->assertInstanceOf( Psr\Log\LoggerInterface::class, $ctx->getLogger() );
+       }
+
+       public function testTypicalRequest() {
+               $ctx = new ResourceLoaderContext( $this->getResourceLoader(), new FauxRequest( [
+                       'debug' => 'false',
+                       'lang' => 'zh',
+                       'modules' => 'foo|foo.quux,baz,bar|baz.quux',
+                       'only' => 'styles',
+                       'skin' => 'fallback',
+               ] ) );
+
+               // Request parameters
+               $this->assertEquals(
+                       $ctx->getModules(),
+                       [ 'foo', 'foo.quux', 'foo.baz', 'foo.bar', 'baz.quux' ]
+               );
+               $this->assertEquals( false, $ctx->getDebug() );
+               $this->assertEquals( 'zh', $ctx->getLanguage() );
+               $this->assertEquals( 'styles', $ctx->getOnly() );
+               $this->assertEquals( 'fallback', $ctx->getSkin() );
+               $this->assertEquals( null, $ctx->getUser() );
+
+               // Misc
+               $this->assertEquals( 'ltr', $ctx->getDirection() );
+               $this->assertEquals( 'zh|fallback|||styles|||||', $ctx->getHash() );
+       }
+
+       public function testShouldInclude() {
+               $ctx = new ResourceLoaderContext( $this->getResourceLoader(), new FauxRequest( [] ) );
+               $this->assertTrue( $ctx->shouldIncludeScripts(), 'Scripts in combined' );
+               $this->assertTrue( $ctx->shouldIncludeStyles(), 'Styles in combined' );
+               $this->assertTrue( $ctx->shouldIncludeMessages(), 'Messages in combined' );
+
+               $ctx = new ResourceLoaderContext( $this->getResourceLoader(), new FauxRequest( [
+                       'only' => 'styles'
+               ] ) );
+               $this->assertFalse( $ctx->shouldIncludeScripts(), 'Scripts not in styles-only' );
+               $this->assertTrue( $ctx->shouldIncludeStyles(), 'Styles in styles-only' );
+               $this->assertFalse( $ctx->shouldIncludeMessages(), 'Messages not in styles-only' );
+
+               $ctx = new ResourceLoaderContext( $this->getResourceLoader(), new FauxRequest( [
+                       'only' => 'scripts'
+               ] ) );
+               $this->assertTrue( $ctx->shouldIncludeScripts(), 'Scripts in scripts-only' );
+               $this->assertFalse( $ctx->shouldIncludeStyles(), 'Styles not in scripts-only' );
+               $this->assertFalse( $ctx->shouldIncludeMessages(), 'Messages not in scripts-only' );
+       }
+
+       public function testGetUser() {
+               $ctx = new ResourceLoaderContext( $this->getResourceLoader(), new FauxRequest( [] ) );
+               $this->assertSame( null, $ctx->getUser() );
+               $this->assertTrue( $ctx->getUserObj()->isAnon() );
+
+               $ctx = new ResourceLoaderContext( $this->getResourceLoader(), new FauxRequest( [
+                       'user' => 'Example'
+               ] ) );
+               $this->assertSame( 'Example', $ctx->getUser() );
+               $this->assertEquals( 'Example', $ctx->getUserObj()->getName() );
+       }
+
+       public function testMsg() {
+               $ctx = new ResourceLoaderContext( $this->getResourceLoader(), new FauxRequest( [
+                       'lang' => 'en'
+               ] ) );
+               $msg = $ctx->msg( 'mainpage' );
+               $this->assertInstanceOf( Message::class, $msg );
+               $this->assertSame( 'Main Page', $msg->useDatabase( false )->plain() );
+       }
+}
diff --git a/tests/phpunit/unit/includes/search/SearchIndexFieldTest.php b/tests/phpunit/unit/includes/search/SearchIndexFieldTest.php
new file mode 100644 (file)
index 0000000..a640c96
--- /dev/null
@@ -0,0 +1,56 @@
+<?php
+
+/**
+ * @group Search
+ * @covers SearchIndexFieldDefinition
+ */
+class SearchIndexFieldTest extends \MediaWikiUnitTestCase {
+
+       public function getMergeCases() {
+               return [
+                       [ 0, 'test', 0, 'test', true ],
+                       [ SearchIndexField::INDEX_TYPE_NESTED, 'test',
+                               SearchIndexField::INDEX_TYPE_NESTED, 'test', false ],
+                       [ 0, 'test', 0, 'test2', true ],
+                       [ 0, 'test', 1, 'test', false ],
+               ];
+       }
+
+       /**
+        * @dataProvider getMergeCases
+        * @param int $t1
+        * @param string $n1
+        * @param int $t2
+        * @param string $n2
+        * @param bool $result
+        */
+       public function testMerge( $t1, $n1, $t2, $n2, $result ) {
+               $field1 =
+                       $this->getMockBuilder( SearchIndexFieldDefinition::class )
+                               ->setMethods( [ 'getMapping' ] )
+                               ->setConstructorArgs( [ $n1, $t1 ] )
+                               ->getMock();
+               $field2 =
+                       $this->getMockBuilder( SearchIndexFieldDefinition::class )
+                               ->setMethods( [ 'getMapping' ] )
+                               ->setConstructorArgs( [ $n2, $t2 ] )
+                               ->getMock();
+
+               if ( $result ) {
+                       $this->assertNotFalse( $field1->merge( $field2 ) );
+               } else {
+                       $this->assertFalse( $field1->merge( $field2 ) );
+               }
+
+               $field1->setFlag( 0xFF );
+               $this->assertFalse( $field1->merge( $field2 ) );
+
+               $field1->setMergeCallback(
+                       function ( $a, $b ) {
+                               return "test";
+                       }
+               );
+               $this->assertEquals( "test", $field1->merge( $field2 ) );
+       }
+
+}
diff --git a/tests/phpunit/unit/includes/search/SearchSuggestionSetTest.php b/tests/phpunit/unit/includes/search/SearchSuggestionSetTest.php
new file mode 100644 (file)
index 0000000..02fa5e9
--- /dev/null
@@ -0,0 +1,111 @@
+<?php
+
+/**
+ * Test for filter utilities.
+ *
+ * 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; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * 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.
+ * http://www.gnu.org/copyleft/gpl.html
+ */
+
+class SearchSuggestionSetTest extends \PHPUnit\Framework\TestCase {
+       /**
+        * Test that adding a new suggestion at the end
+        * will keep proper score ordering
+        * @covers SearchSuggestionSet::append
+        */
+       public function testAppend() {
+               $set = SearchSuggestionSet::emptySuggestionSet();
+               $this->assertEquals( 0, $set->getSize() );
+               $set->append( new SearchSuggestion( 3 ) );
+               $this->assertEquals( 3, $set->getWorstScore() );
+               $this->assertEquals( 3, $set->getBestScore() );
+
+               $suggestion = new SearchSuggestion( 4 );
+               $set->append( $suggestion );
+               $this->assertEquals( 2, $set->getWorstScore() );
+               $this->assertEquals( 3, $set->getBestScore() );
+               $this->assertEquals( 2, $suggestion->getScore() );
+
+               $suggestion = new SearchSuggestion( 2 );
+               $set->append( $suggestion );
+               $this->assertEquals( 1, $set->getWorstScore() );
+               $this->assertEquals( 3, $set->getBestScore() );
+               $this->assertEquals( 1, $suggestion->getScore() );
+
+               $scores = $set->map( function ( $s ) {
+                       return $s->getScore();
+               } );
+               $sorted = $scores;
+               asort( $sorted );
+               $this->assertEquals( $sorted, $scores );
+       }
+
+       /**
+        * Test that adding a new best suggestion will keep proper score
+        * ordering
+        * @covers SearchSuggestionSet::getWorstScore
+        * @covers SearchSuggestionSet::getBestScore
+        * @covers SearchSuggestionSet::prepend
+        */
+       public function testInsertBest() {
+               $set = SearchSuggestionSet::emptySuggestionSet();
+               $this->assertEquals( 0, $set->getSize() );
+               $set->prepend( new SearchSuggestion( 3 ) );
+               $this->assertEquals( 3, $set->getWorstScore() );
+               $this->assertEquals( 3, $set->getBestScore() );
+
+               $suggestion = new SearchSuggestion( 4 );
+               $set->prepend( $suggestion );
+               $this->assertEquals( 3, $set->getWorstScore() );
+               $this->assertEquals( 4, $set->getBestScore() );
+               $this->assertEquals( 4, $suggestion->getScore() );
+
+               $suggestion = new SearchSuggestion( 0 );
+               $set->prepend( $suggestion );
+               $this->assertEquals( 3, $set->getWorstScore() );
+               $this->assertEquals( 5, $set->getBestScore() );
+               $this->assertEquals( 5, $suggestion->getScore() );
+
+               $suggestion = new SearchSuggestion( 2 );
+               $set->prepend( $suggestion );
+               $this->assertEquals( 3, $set->getWorstScore() );
+               $this->assertEquals( 6, $set->getBestScore() );
+               $this->assertEquals( 6, $suggestion->getScore() );
+
+               $scores = $set->map( function ( $s ) {
+                       return $s->getScore();
+               } );
+               $sorted = $scores;
+               asort( $sorted );
+               $this->assertEquals( $sorted, $scores );
+       }
+
+       /**
+        * @covers SearchSuggestionSet::shrink
+        */
+       public function testShrink() {
+               $set = SearchSuggestionSet::emptySuggestionSet();
+               for ( $i = 0; $i < 100; $i++ ) {
+                       $set->append( new SearchSuggestion( 0 ) );
+               }
+               $set->shrink( 10 );
+               $this->assertEquals( 10, $set->getSize() );
+
+               $set->shrink( 0 );
+               $this->assertEquals( 0, $set->getSize() );
+       }
+
+       // TODO: test for fromTitles
+}
diff --git a/tests/phpunit/unit/includes/session/MetadataMergeExceptionTest.php b/tests/phpunit/unit/includes/session/MetadataMergeExceptionTest.php
new file mode 100644 (file)
index 0000000..707adfe
--- /dev/null
@@ -0,0 +1,28 @@
+<?php
+
+namespace MediaWiki\Session;
+
+/**
+ * @group Session
+ * @covers MediaWiki\Session\MetadataMergeException
+ */
+class MetadataMergeExceptionTest extends \MediaWikiUnitTestCase {
+
+       public function testBasics() {
+               $data = [ 'foo' => 'bar' ];
+
+               $ex = new MetadataMergeException();
+               $this->assertInstanceOf( \UnexpectedValueException::class, $ex );
+               $this->assertSame( [], $ex->getContext() );
+
+               $ex2 = new MetadataMergeException( 'Message', 42, $ex, $data );
+               $this->assertSame( 'Message', $ex2->getMessage() );
+               $this->assertSame( 42, $ex2->getCode() );
+               $this->assertSame( $ex, $ex2->getPrevious() );
+               $this->assertSame( $data, $ex2->getContext() );
+
+               $ex->setContext( $data );
+               $this->assertSame( $data, $ex->getContext() );
+       }
+
+}
diff --git a/tests/phpunit/unit/includes/session/SessionIdTest.php b/tests/phpunit/unit/includes/session/SessionIdTest.php
new file mode 100644 (file)
index 0000000..3c7f8cb
--- /dev/null
@@ -0,0 +1,20 @@
+<?php
+
+namespace MediaWiki\Session;
+
+/**
+ * @group Session
+ * @covers MediaWiki\Session\SessionId
+ */
+class SessionIdTest extends \MediaWikiUnitTestCase {
+
+       public function testEverything() {
+               $id = new SessionId( 'foo' );
+               $this->assertSame( 'foo', $id->getId() );
+               $this->assertSame( 'foo', (string)$id );
+               $id->setId( 'bar' );
+               $this->assertSame( 'bar', $id->getId() );
+               $this->assertSame( 'bar', (string)$id );
+       }
+
+}
diff --git a/tests/phpunit/unit/includes/session/SessionInfoTest.php b/tests/phpunit/unit/includes/session/SessionInfoTest.php
new file mode 100644 (file)
index 0000000..a3a6365
--- /dev/null
@@ -0,0 +1,357 @@
+<?php
+
+namespace MediaWiki\Session;
+
+/**
+ * @group Session
+ * @group Database
+ * @covers MediaWiki\Session\SessionInfo
+ */
+class SessionInfoTest extends \MediaWikiUnitTestCase {
+
+       public function testBasics() {
+               $sysopUser = new \User();
+               $sysopUser->setName( 'UTSysop' );
+
+               $anonInfo = UserInfo::newAnonymous();
+               $userInfo = UserInfo::newFromUser( $sysopUser, true );
+               $unverifiedUserInfo = UserInfo::newFromUser( $sysopUser, false );
+
+               try {
+                       new SessionInfo( SessionInfo::MIN_PRIORITY - 1, [] );
+                       $this->fail( 'Expected exception not thrown', 'priority < min' );
+               } catch ( \InvalidArgumentException $ex ) {
+                       $this->assertSame( 'Invalid priority', $ex->getMessage(), 'priority < min' );
+               }
+
+               try {
+                       new SessionInfo( SessionInfo::MAX_PRIORITY + 1, [] );
+                       $this->fail( 'Expected exception not thrown', 'priority > max' );
+               } catch ( \InvalidArgumentException $ex ) {
+                       $this->assertSame( 'Invalid priority', $ex->getMessage(), 'priority > max' );
+               }
+
+               try {
+                       new SessionInfo( SessionInfo::MIN_PRIORITY, [ 'id' => 'ABC?' ] );
+                       $this->fail( 'Expected exception not thrown', 'bad session ID' );
+               } catch ( \InvalidArgumentException $ex ) {
+                       $this->assertSame( 'Invalid session ID', $ex->getMessage(), 'bad session ID' );
+               }
+
+               try {
+                       new SessionInfo( SessionInfo::MIN_PRIORITY, [ 'userInfo' => new \stdClass ] );
+                       $this->fail( 'Expected exception not thrown', 'bad userInfo' );
+               } catch ( \InvalidArgumentException $ex ) {
+                       $this->assertSame( 'Invalid userInfo', $ex->getMessage(), 'bad userInfo' );
+               }
+
+               try {
+                       new SessionInfo( SessionInfo::MIN_PRIORITY, [] );
+                       $this->fail( 'Expected exception not thrown', 'no provider, no id' );
+               } catch ( \InvalidArgumentException $ex ) {
+                       $this->assertSame( 'Must supply an ID when no provider is given', $ex->getMessage(),
+                               'no provider, no id' );
+               }
+
+               try {
+                       new SessionInfo( SessionInfo::MIN_PRIORITY, [ 'copyFrom' => new \stdClass ] );
+                       $this->fail( 'Expected exception not thrown', 'bad copyFrom' );
+               } catch ( \InvalidArgumentException $ex ) {
+                       $this->assertSame( 'Invalid copyFrom', $ex->getMessage(),
+                               'bad copyFrom' );
+               }
+
+               $manager = new SessionManager();
+               $provider = $this->getMockBuilder( SessionProvider::class )
+                       ->setMethods( [ 'persistsSessionId', 'canChangeUser', '__toString' ] )
+                       ->getMockForAbstractClass();
+               $provider->setManager( $manager );
+               $provider->expects( $this->any() )->method( 'persistsSessionId' )
+                       ->will( $this->returnValue( true ) );
+               $provider->expects( $this->any() )->method( 'canChangeUser' )
+                       ->will( $this->returnValue( true ) );
+               $provider->expects( $this->any() )->method( '__toString' )
+                       ->will( $this->returnValue( 'Mock' ) );
+
+               $provider2 = $this->getMockBuilder( SessionProvider::class )
+                       ->setMethods( [ 'persistsSessionId', 'canChangeUser', '__toString' ] )
+                       ->getMockForAbstractClass();
+               $provider2->setManager( $manager );
+               $provider2->expects( $this->any() )->method( 'persistsSessionId' )
+                       ->will( $this->returnValue( true ) );
+               $provider2->expects( $this->any() )->method( 'canChangeUser' )
+                       ->will( $this->returnValue( true ) );
+               $provider2->expects( $this->any() )->method( '__toString' )
+                       ->will( $this->returnValue( 'Mock2' ) );
+
+               try {
+                       new SessionInfo( SessionInfo::MIN_PRIORITY, [
+                               'provider' => $provider,
+                               'userInfo' => $anonInfo,
+                               'metadata' => 'foo',
+                       ] );
+                       $this->fail( 'Expected exception not thrown', 'bad metadata' );
+               } catch ( \InvalidArgumentException $ex ) {
+                       $this->assertSame( 'Invalid metadata', $ex->getMessage(), 'bad metadata' );
+               }
+
+               $info = new SessionInfo( SessionInfo::MIN_PRIORITY + 5, [
+                       'provider' => $provider,
+                       'userInfo' => $anonInfo
+               ] );
+               $this->assertSame( $provider, $info->getProvider() );
+               $this->assertNotNull( $info->getId() );
+               $this->assertSame( SessionInfo::MIN_PRIORITY + 5, $info->getPriority() );
+               $this->assertSame( $anonInfo, $info->getUserInfo() );
+               $this->assertTrue( $info->isIdSafe() );
+               $this->assertFalse( $info->forceUse() );
+               $this->assertFalse( $info->wasPersisted() );
+               $this->assertFalse( $info->wasRemembered() );
+               $this->assertFalse( $info->forceHTTPS() );
+               $this->assertNull( $info->getProviderMetadata() );
+
+               $info = new SessionInfo( SessionInfo::MIN_PRIORITY + 5, [
+                       'provider' => $provider,
+                       'userInfo' => $unverifiedUserInfo,
+                       'metadata' => [ 'Foo' ],
+               ] );
+               $this->assertSame( $provider, $info->getProvider() );
+               $this->assertNotNull( $info->getId() );
+               $this->assertSame( SessionInfo::MIN_PRIORITY + 5, $info->getPriority() );
+               $this->assertSame( $unverifiedUserInfo, $info->getUserInfo() );
+               $this->assertTrue( $info->isIdSafe() );
+               $this->assertFalse( $info->forceUse() );
+               $this->assertFalse( $info->wasPersisted() );
+               $this->assertFalse( $info->wasRemembered() );
+               $this->assertFalse( $info->forceHTTPS() );
+               $this->assertSame( [ 'Foo' ], $info->getProviderMetadata() );
+
+               $info = new SessionInfo( SessionInfo::MIN_PRIORITY + 5, [
+                       'provider' => $provider,
+                       'userInfo' => $userInfo
+               ] );
+               $this->assertSame( $provider, $info->getProvider() );
+               $this->assertNotNull( $info->getId() );
+               $this->assertSame( SessionInfo::MIN_PRIORITY + 5, $info->getPriority() );
+               $this->assertSame( $userInfo, $info->getUserInfo() );
+               $this->assertTrue( $info->isIdSafe() );
+               $this->assertFalse( $info->forceUse() );
+               $this->assertFalse( $info->wasPersisted() );
+               $this->assertTrue( $info->wasRemembered() );
+               $this->assertFalse( $info->forceHTTPS() );
+               $this->assertNull( $info->getProviderMetadata() );
+
+               $id = $manager->generateSessionId();
+
+               $info = new SessionInfo( SessionInfo::MIN_PRIORITY + 5, [
+                       'provider' => $provider,
+                       'id' => $id,
+                       'persisted' => true,
+                       'userInfo' => $anonInfo
+               ] );
+               $this->assertSame( $provider, $info->getProvider() );
+               $this->assertSame( $id, $info->getId() );
+               $this->assertSame( SessionInfo::MIN_PRIORITY + 5, $info->getPriority() );
+               $this->assertSame( $anonInfo, $info->getUserInfo() );
+               $this->assertFalse( $info->isIdSafe() );
+               $this->assertFalse( $info->forceUse() );
+               $this->assertTrue( $info->wasPersisted() );
+               $this->assertFalse( $info->wasRemembered() );
+               $this->assertFalse( $info->forceHTTPS() );
+               $this->assertNull( $info->getProviderMetadata() );
+
+               $info = new SessionInfo( SessionInfo::MIN_PRIORITY + 5, [
+                       'provider' => $provider,
+                       'id' => $id,
+                       'userInfo' => $userInfo
+               ] );
+               $this->assertSame( $provider, $info->getProvider() );
+               $this->assertSame( $id, $info->getId() );
+               $this->assertSame( SessionInfo::MIN_PRIORITY + 5, $info->getPriority() );
+               $this->assertSame( $userInfo, $info->getUserInfo() );
+               $this->assertFalse( $info->isIdSafe() );
+               $this->assertFalse( $info->forceUse() );
+               $this->assertFalse( $info->wasPersisted() );
+               $this->assertTrue( $info->wasRemembered() );
+               $this->assertFalse( $info->forceHTTPS() );
+               $this->assertNull( $info->getProviderMetadata() );
+
+               $info = new SessionInfo( SessionInfo::MIN_PRIORITY + 5, [
+                       'id' => $id,
+                       'persisted' => true,
+                       'userInfo' => $userInfo,
+                       'metadata' => [ 'Foo' ],
+               ] );
+               $this->assertSame( $id, $info->getId() );
+               $this->assertSame( SessionInfo::MIN_PRIORITY + 5, $info->getPriority() );
+               $this->assertSame( $userInfo, $info->getUserInfo() );
+               $this->assertFalse( $info->isIdSafe() );
+               $this->assertFalse( $info->forceUse() );
+               $this->assertTrue( $info->wasPersisted() );
+               $this->assertFalse( $info->wasRemembered() );
+               $this->assertFalse( $info->forceHTTPS() );
+               $this->assertNull( $info->getProviderMetadata() );
+
+               $info = new SessionInfo( SessionInfo::MIN_PRIORITY + 5, [
+                       'id' => $id,
+                       'remembered' => true,
+                       'userInfo' => $userInfo,
+               ] );
+               $this->assertFalse( $info->wasRemembered(), 'no provider' );
+
+               $info = new SessionInfo( SessionInfo::MIN_PRIORITY + 5, [
+                       'provider' => $provider,
+                       'id' => $id,
+                       'remembered' => true,
+               ] );
+               $this->assertFalse( $info->wasRemembered(), 'no user' );
+
+               $info = new SessionInfo( SessionInfo::MIN_PRIORITY + 5, [
+                       'provider' => $provider,
+                       'id' => $id,
+                       'remembered' => true,
+                       'userInfo' => $anonInfo,
+               ] );
+               $this->assertFalse( $info->wasRemembered(), 'anonymous user' );
+
+               $info = new SessionInfo( SessionInfo::MIN_PRIORITY + 5, [
+                       'provider' => $provider,
+                       'id' => $id,
+                       'remembered' => true,
+                       'userInfo' => $unverifiedUserInfo,
+               ] );
+               $this->assertFalse( $info->wasRemembered(), 'unverified user' );
+
+               $info = new SessionInfo( SessionInfo::MIN_PRIORITY + 5, [
+                       'provider' => $provider,
+                       'id' => $id,
+                       'remembered' => false,
+                       'userInfo' => $userInfo,
+               ] );
+               $this->assertFalse( $info->wasRemembered(), 'specific override' );
+
+               $info = new SessionInfo( SessionInfo::MIN_PRIORITY + 5, [
+                       'id' => $id,
+                       'idIsSafe' => true,
+               ] );
+               $this->assertSame( $id, $info->getId() );
+               $this->assertSame( SessionInfo::MIN_PRIORITY + 5, $info->getPriority() );
+               $this->assertTrue( $info->isIdSafe() );
+
+               $info = new SessionInfo( SessionInfo::MIN_PRIORITY + 5, [
+                       'id' => $id,
+                       'forceUse' => true,
+               ] );
+               $this->assertFalse( $info->forceUse(), 'no provider' );
+
+               $info = new SessionInfo( SessionInfo::MIN_PRIORITY + 5, [
+                       'provider' => $provider,
+                       'forceUse' => true,
+               ] );
+               $this->assertFalse( $info->forceUse(), 'no id' );
+
+               $info = new SessionInfo( SessionInfo::MIN_PRIORITY + 5, [
+                       'provider' => $provider,
+                       'id' => $id,
+                       'forceUse' => true,
+               ] );
+               $this->assertTrue( $info->forceUse(), 'correct use' );
+
+               $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [
+                       'id' => $id,
+                       'forceHTTPS' => 1,
+               ] );
+               $this->assertTrue( $info->forceHTTPS() );
+
+               $fromInfo = new SessionInfo( SessionInfo::MIN_PRIORITY, [
+                       'id' => $id . 'A',
+                       'provider' => $provider,
+                       'userInfo' => $userInfo,
+                       'idIsSafe' => true,
+                       'forceUse' => true,
+                       'persisted' => true,
+                       'remembered' => true,
+                       'forceHTTPS' => true,
+                       'metadata' => [ 'foo!' ],
+               ] );
+               $info = new SessionInfo( SessionInfo::MIN_PRIORITY + 4, [
+                       'copyFrom' => $fromInfo,
+               ] );
+               $this->assertSame( $id . 'A', $info->getId() );
+               $this->assertSame( SessionInfo::MIN_PRIORITY + 4, $info->getPriority() );
+               $this->assertSame( $provider, $info->getProvider() );
+               $this->assertSame( $userInfo, $info->getUserInfo() );
+               $this->assertTrue( $info->isIdSafe() );
+               $this->assertTrue( $info->forceUse() );
+               $this->assertTrue( $info->wasPersisted() );
+               $this->assertTrue( $info->wasRemembered() );
+               $this->assertTrue( $info->forceHTTPS() );
+               $this->assertSame( [ 'foo!' ], $info->getProviderMetadata() );
+
+               $info = new SessionInfo( SessionInfo::MIN_PRIORITY + 4, [
+                       'id' => $id . 'X',
+                       'provider' => $provider2,
+                       'userInfo' => $unverifiedUserInfo,
+                       'idIsSafe' => false,
+                       'forceUse' => false,
+                       'persisted' => false,
+                       'remembered' => false,
+                       'forceHTTPS' => false,
+                       'metadata' => null,
+                       'copyFrom' => $fromInfo,
+               ] );
+               $this->assertSame( $id . 'X', $info->getId() );
+               $this->assertSame( SessionInfo::MIN_PRIORITY + 4, $info->getPriority() );
+               $this->assertSame( $provider2, $info->getProvider() );
+               $this->assertSame( $unverifiedUserInfo, $info->getUserInfo() );
+               $this->assertFalse( $info->isIdSafe() );
+               $this->assertFalse( $info->forceUse() );
+               $this->assertFalse( $info->wasPersisted() );
+               $this->assertFalse( $info->wasRemembered() );
+               $this->assertFalse( $info->forceHTTPS() );
+               $this->assertNull( $info->getProviderMetadata() );
+
+               $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [
+                       'id' => $id,
+               ] );
+               $this->assertSame(
+                       '[' . SessionInfo::MIN_PRIORITY . "]null<null>$id",
+                       (string)$info,
+                       'toString'
+               );
+
+               $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [
+                       'provider' => $provider,
+                       'id' => $id,
+                       'persisted' => true,
+                       'userInfo' => $userInfo
+               ] );
+               $this->assertSame(
+                       '[' . SessionInfo::MIN_PRIORITY . "]Mock<+:{$userInfo->getId()}:UTSysop>$id",
+                       (string)$info,
+                       'toString'
+               );
+
+               $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [
+                       'provider' => $provider,
+                       'id' => $id,
+                       'persisted' => true,
+                       'userInfo' => $unverifiedUserInfo
+               ] );
+               $this->assertSame(
+                       '[' . SessionInfo::MIN_PRIORITY . "]Mock<-:{$userInfo->getId()}:UTSysop>$id",
+                       (string)$info,
+                       'toString'
+               );
+       }
+
+       public function testCompare() {
+               $id = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
+               $info1 = new SessionInfo( SessionInfo::MIN_PRIORITY + 1, [ 'id' => $id ] );
+               $info2 = new SessionInfo( SessionInfo::MIN_PRIORITY + 2, [ 'id' => $id ] );
+
+               $this->assertTrue( SessionInfo::compare( $info1, $info2 ) < 0, '<' );
+               $this->assertTrue( SessionInfo::compare( $info2, $info1 ) > 0, '>' );
+               $this->assertTrue( SessionInfo::compare( $info1, $info1 ) === 0, '==' );
+       }
+}
diff --git a/tests/phpunit/unit/includes/session/SessionProviderTest.php b/tests/phpunit/unit/includes/session/SessionProviderTest.php
new file mode 100644 (file)
index 0000000..114fa24
--- /dev/null
@@ -0,0 +1,205 @@
+<?php
+
+namespace MediaWiki\Session;
+
+use Wikimedia\TestingAccessWrapper;
+
+/**
+ * @group Session
+ * @group Database
+ * @covers MediaWiki\Session\SessionProvider
+ */
+class SessionProviderTest extends \MediaWikiUnitTestCase {
+
+       public function testBasics() {
+               $manager = new SessionManager();
+               $logger = new \TestLogger();
+               $config = new \HashConfig();
+
+               $provider = $this->getMockForAbstractClass( SessionProvider::class );
+               $priv = TestingAccessWrapper::newFromObject( $provider );
+
+               $provider->setConfig( $config );
+               $this->assertSame( $config, $priv->config );
+               $provider->setLogger( $logger );
+               $this->assertSame( $logger, $priv->logger );
+               $provider->setManager( $manager );
+               $this->assertSame( $manager, $priv->manager );
+               $this->assertSame( $manager, $provider->getManager() );
+
+               $provider->invalidateSessionsForUser( new \User );
+
+               $this->assertSame( [], $provider->getVaryHeaders() );
+               $this->assertSame( [], $provider->getVaryCookies() );
+               $this->assertSame( null, $provider->suggestLoginUsername( new \FauxRequest ) );
+
+               $this->assertSame( get_class( $provider ), (string)$provider );
+
+               $this->assertNull( $provider->getRememberUserDuration() );
+
+               $this->assertNull( $provider->whyNoSession() );
+
+               $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [
+                       'id' => 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
+                       'provider' => $provider,
+               ] );
+               $metadata = [ 'foo' ];
+               $this->assertTrue( $provider->refreshSessionInfo( $info, new \FauxRequest, $metadata ) );
+               $this->assertSame( [ 'foo' ], $metadata );
+       }
+
+       /**
+        * @dataProvider provideNewSessionInfo
+        * @param bool $persistId Return value for ->persistsSessionId()
+        * @param bool $persistUser Return value for ->persistsSessionUser()
+        * @param bool $ok Whether a SessionInfo is provided
+        */
+       public function testNewSessionInfo( $persistId, $persistUser, $ok ) {
+               $manager = new SessionManager();
+
+               $provider = $this->getMockBuilder( SessionProvider::class )
+                       ->setMethods( [ 'canChangeUser', 'persistsSessionId' ] )
+                       ->getMockForAbstractClass();
+               $provider->expects( $this->any() )->method( 'persistsSessionId' )
+                       ->will( $this->returnValue( $persistId ) );
+               $provider->expects( $this->any() )->method( 'canChangeUser' )
+                       ->will( $this->returnValue( $persistUser ) );
+               $provider->setManager( $manager );
+
+               if ( $ok ) {
+                       $info = $provider->newSessionInfo();
+                       $this->assertNotNull( $info );
+                       $this->assertFalse( $info->wasPersisted() );
+                       $this->assertTrue( $info->isIdSafe() );
+
+                       $id = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
+                       $info = $provider->newSessionInfo( $id );
+                       $this->assertNotNull( $info );
+                       $this->assertSame( $id, $info->getId() );
+                       $this->assertFalse( $info->wasPersisted() );
+                       $this->assertTrue( $info->isIdSafe() );
+               } else {
+                       $this->assertNull( $provider->newSessionInfo() );
+               }
+       }
+
+       public function testMergeMetadata() {
+               $provider = $this->getMockBuilder( SessionProvider::class )
+                       ->getMockForAbstractClass();
+
+               try {
+                       $provider->mergeMetadata(
+                               [ 'foo' => 1, 'baz' => 3 ],
+                               [ 'bar' => 2, 'baz' => '3' ]
+                       );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( MetadataMergeException $ex ) {
+                       $this->assertSame( 'Key "baz" changed', $ex->getMessage() );
+                       $this->assertSame(
+                               [ 'old_value' => 3, 'new_value' => '3' ], $ex->getContext() );
+               }
+
+               $res = $provider->mergeMetadata(
+                       [ 'foo' => 1, 'baz' => 3 ],
+                       [ 'bar' => 2, 'baz' => 3 ]
+               );
+               $this->assertSame( [ 'bar' => 2, 'baz' => 3 ], $res );
+       }
+
+       public static function provideNewSessionInfo() {
+               return [
+                       [ false, false, false ],
+                       [ true, false, false ],
+                       [ false, true, false ],
+                       [ true, true, true ],
+               ];
+       }
+
+       public function testImmutableSessions() {
+               $provider = $this->getMockBuilder( SessionProvider::class )
+                       ->setMethods( [ 'canChangeUser', 'persistsSessionId' ] )
+                       ->getMockForAbstractClass();
+               $provider->expects( $this->any() )->method( 'canChangeUser' )
+                       ->will( $this->returnValue( true ) );
+               $provider->preventSessionsForUser( 'Foo' );
+
+               $provider = $this->getMockBuilder( SessionProvider::class )
+                       ->setMethods( [ 'canChangeUser', 'persistsSessionId' ] )
+                       ->getMockForAbstractClass();
+               $provider->expects( $this->any() )->method( 'canChangeUser' )
+                       ->will( $this->returnValue( false ) );
+               try {
+                       $provider->preventSessionsForUser( 'Foo' );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( \BadMethodCallException $ex ) {
+                       $this->assertSame(
+                               'MediaWiki\\Session\\SessionProvider::preventSessionsForUser must be implemented ' .
+                                       'when canChangeUser() is false',
+                               $ex->getMessage()
+                       );
+               }
+       }
+
+       public function testHashToSessionId() {
+               $config = new \HashConfig( [
+                       'SecretKey' => 'Shhh!',
+               ] );
+
+               $provider = $this->getMockForAbstractClass( SessionProvider::class,
+                       [], 'MockSessionProvider' );
+               $provider->setConfig( $config );
+               $priv = TestingAccessWrapper::newFromObject( $provider );
+
+               $this->assertSame( 'eoq8cb1mg7j30ui5qolafps4hg29k5bb', $priv->hashToSessionId( 'foobar' ) );
+               $this->assertSame( '4do8j7tfld1g8tte9jqp3csfgmulaun9',
+                       $priv->hashToSessionId( 'foobar', 'secret' ) );
+
+               try {
+                       $priv->hashToSessionId( [] );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( \InvalidArgumentException $ex ) {
+                       $this->assertSame(
+                               '$data must be a string, array was passed',
+                               $ex->getMessage()
+                       );
+               }
+               try {
+                       $priv->hashToSessionId( '', false );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( \InvalidArgumentException $ex ) {
+                       $this->assertSame(
+                               '$key must be a string or null, boolean was passed',
+                               $ex->getMessage()
+                       );
+               }
+       }
+
+       public function testDescribe() {
+               $provider = $this->getMockForAbstractClass( SessionProvider::class,
+                       [], 'MockSessionProvider' );
+
+               $this->assertSame(
+                       'MockSessionProvider sessions',
+                       $provider->describe( \Language::factory( 'en' ) )
+               );
+       }
+
+       public function testGetAllowedUserRights() {
+               $provider = $this->getMockForAbstractClass( SessionProvider::class );
+               $backend = TestUtils::getDummySessionBackend();
+
+               try {
+                       $provider->getAllowedUserRights( $backend );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( \InvalidArgumentException $ex ) {
+                       $this->assertSame(
+                               'Backend\'s provider isn\'t $this',
+                               $ex->getMessage()
+                       );
+               }
+
+               TestingAccessWrapper::newFromObject( $backend )->provider = $provider;
+               $this->assertNull( $provider->getAllowedUserRights( $backend ) );
+       }
+
+}
diff --git a/tests/phpunit/unit/includes/session/SessionTest.php b/tests/phpunit/unit/includes/session/SessionTest.php
new file mode 100644 (file)
index 0000000..73bf060
--- /dev/null
@@ -0,0 +1,372 @@
+<?php
+
+namespace MediaWiki\Session;
+
+use Psr\Log\LogLevel;
+use User;
+use Wikimedia\TestingAccessWrapper;
+
+/**
+ * @group Session
+ * @covers MediaWiki\Session\Session
+ */
+class SessionTest extends \MediaWikiUnitTestCase {
+
+       public function testConstructor() {
+               $backend = TestUtils::getDummySessionBackend();
+               TestingAccessWrapper::newFromObject( $backend )->requests = [ -1 => 'dummy' ];
+               TestingAccessWrapper::newFromObject( $backend )->id = new SessionId( 'abc' );
+
+               $session = new Session( $backend, 42, new \TestLogger );
+               $priv = TestingAccessWrapper::newFromObject( $session );
+               $this->assertSame( $backend, $priv->backend );
+               $this->assertSame( 42, $priv->index );
+
+               $request = new \FauxRequest();
+               $priv2 = TestingAccessWrapper::newFromObject( $session->sessionWithRequest( $request ) );
+               $this->assertSame( $backend, $priv2->backend );
+               $this->assertNotSame( $priv->index, $priv2->index );
+               $this->assertSame( $request, $priv2->getRequest() );
+       }
+
+       /**
+        * @dataProvider provideMethods
+        * @param string $m Method to test
+        * @param array $args Arguments to pass to the method
+        * @param bool $index Whether the backend method gets passed the index
+        * @param bool $ret Whether the method returns a value
+        */
+       public function testMethods( $m, $args, $index, $ret ) {
+               $mock = $this->getMockBuilder( DummySessionBackend::class )
+                       ->setMethods( [ $m, 'deregisterSession' ] )
+                       ->getMock();
+               $mock->expects( $this->once() )->method( 'deregisterSession' )
+                       ->with( $this->identicalTo( 42 ) );
+
+               $tmp = $mock->expects( $this->once() )->method( $m );
+               $expectArgs = [];
+               if ( $index ) {
+                       $expectArgs[] = $this->identicalTo( 42 );
+               }
+               foreach ( $args as $arg ) {
+                       $expectArgs[] = $this->identicalTo( $arg );
+               }
+               $tmp = call_user_func_array( [ $tmp, 'with' ], $expectArgs );
+
+               $retval = new \stdClass;
+               $tmp->will( $this->returnValue( $retval ) );
+
+               $session = TestUtils::getDummySession( $mock, 42 );
+
+               if ( $ret ) {
+                       $this->assertSame( $retval, call_user_func_array( [ $session, $m ], $args ) );
+               } else {
+                       $this->assertNull( call_user_func_array( [ $session, $m ], $args ) );
+               }
+
+               // Trigger Session destructor
+               $session = null;
+       }
+
+       public static function provideMethods() {
+               return [
+                       [ 'getId', [], false, true ],
+                       [ 'getSessionId', [], false, true ],
+                       [ 'resetId', [], false, true ],
+                       [ 'getProvider', [], false, true ],
+                       [ 'isPersistent', [], false, true ],
+                       [ 'persist', [], false, false ],
+                       [ 'unpersist', [], false, false ],
+                       [ 'shouldRememberUser', [], false, true ],
+                       [ 'setRememberUser', [ true ], false, false ],
+                       [ 'getRequest', [], true, true ],
+                       [ 'getUser', [], false, true ],
+                       [ 'getAllowedUserRights', [], false, true ],
+                       [ 'canSetUser', [], false, true ],
+                       [ 'setUser', [ new \stdClass ], false, false ],
+                       [ 'suggestLoginUsername', [], true, true ],
+                       [ 'shouldForceHTTPS', [], false, true ],
+                       [ 'setForceHTTPS', [ true ], false, false ],
+                       [ 'getLoggedOutTimestamp', [], false, true ],
+                       [ 'setLoggedOutTimestamp', [ 123 ], false, false ],
+                       [ 'getProviderMetadata', [], false, true ],
+                       [ 'save', [], false, false ],
+                       [ 'delaySave', [], false, true ],
+                       [ 'renew', [], false, false ],
+               ];
+       }
+
+       public function testDataAccess() {
+               $session = TestUtils::getDummySession();
+               $backend = TestingAccessWrapper::newFromObject( $session )->backend;
+
+               $this->assertEquals( 1, $session->get( 'foo' ) );
+               $this->assertEquals( 'zero', $session->get( 0 ) );
+               $this->assertFalse( $backend->dirty );
+
+               $this->assertEquals( null, $session->get( 'null' ) );
+               $this->assertEquals( 'default', $session->get( 'null', 'default' ) );
+               $this->assertFalse( $backend->dirty );
+
+               $session->set( 'foo', 55 );
+               $this->assertEquals( 55, $backend->data['foo'] );
+               $this->assertTrue( $backend->dirty );
+               $backend->dirty = false;
+
+               $session->set( 1, 'one' );
+               $this->assertEquals( 'one', $backend->data[1] );
+               $this->assertTrue( $backend->dirty );
+               $backend->dirty = false;
+
+               $session->set( 1, 'one' );
+               $this->assertFalse( $backend->dirty );
+
+               $this->assertTrue( $session->exists( 'foo' ) );
+               $this->assertTrue( $session->exists( 1 ) );
+               $this->assertFalse( $session->exists( 'null' ) );
+               $this->assertFalse( $session->exists( 100 ) );
+               $this->assertFalse( $backend->dirty );
+
+               $session->remove( 'foo' );
+               $this->assertArrayNotHasKey( 'foo', $backend->data );
+               $this->assertTrue( $backend->dirty );
+               $backend->dirty = false;
+               $session->remove( 1 );
+               $this->assertArrayNotHasKey( 1, $backend->data );
+               $this->assertTrue( $backend->dirty );
+               $backend->dirty = false;
+
+               $session->remove( 101 );
+               $this->assertFalse( $backend->dirty );
+
+               $backend->data = [ 'a', 'b', '?' => 'c' ];
+               $this->assertSame( 3, $session->count() );
+               $this->assertSame( 3, count( $session ) );
+               $this->assertFalse( $backend->dirty );
+
+               $data = [];
+               foreach ( $session as $key => $value ) {
+                       $data[$key] = $value;
+               }
+               $this->assertEquals( $backend->data, $data );
+               $this->assertFalse( $backend->dirty );
+
+               $this->assertEquals( $backend->data, iterator_to_array( $session ) );
+               $this->assertFalse( $backend->dirty );
+       }
+
+       public function testArrayAccess() {
+               $logger = new \TestLogger;
+               $session = TestUtils::getDummySession( null, -1, $logger );
+               $backend = TestingAccessWrapper::newFromObject( $session )->backend;
+
+               $this->assertEquals( 1, $session['foo'] );
+               $this->assertEquals( 'zero', $session[0] );
+               $this->assertFalse( $backend->dirty );
+
+               $logger->setCollect( true );
+               $this->assertEquals( null, $session['null'] );
+               $logger->setCollect( false );
+               $this->assertFalse( $backend->dirty );
+               $this->assertSame( [
+                       [ LogLevel::DEBUG, 'Undefined index (auto-adds to session with a null value): null' ]
+               ], $logger->getBuffer() );
+               $logger->clearBuffer();
+
+               $session['foo'] = 55;
+               $this->assertEquals( 55, $backend->data['foo'] );
+               $this->assertTrue( $backend->dirty );
+               $backend->dirty = false;
+
+               $session[1] = 'one';
+               $this->assertEquals( 'one', $backend->data[1] );
+               $this->assertTrue( $backend->dirty );
+               $backend->dirty = false;
+
+               $session[1] = 'one';
+               $this->assertFalse( $backend->dirty );
+
+               $session['bar'] = [ 'baz' => [] ];
+               $session['bar']['baz']['quux'] = 2;
+               $this->assertEquals( [ 'baz' => [ 'quux' => 2 ] ], $backend->data['bar'] );
+
+               $logger->setCollect( true );
+               $session['bar2']['baz']['quux'] = 3;
+               $logger->setCollect( false );
+               $this->assertEquals( [ 'baz' => [ 'quux' => 3 ] ], $backend->data['bar2'] );
+               $this->assertSame( [
+                       [ LogLevel::DEBUG, 'Undefined index (auto-adds to session with a null value): bar2' ]
+               ], $logger->getBuffer() );
+               $logger->clearBuffer();
+
+               $backend->dirty = false;
+               $this->assertTrue( isset( $session['foo'] ) );
+               $this->assertTrue( isset( $session[1] ) );
+               $this->assertFalse( isset( $session['null'] ) );
+               $this->assertFalse( isset( $session['missing'] ) );
+               $this->assertFalse( isset( $session[100] ) );
+               $this->assertFalse( $backend->dirty );
+
+               unset( $session['foo'] );
+               $this->assertArrayNotHasKey( 'foo', $backend->data );
+               $this->assertTrue( $backend->dirty );
+               $backend->dirty = false;
+               unset( $session[1] );
+               $this->assertArrayNotHasKey( 1, $backend->data );
+               $this->assertTrue( $backend->dirty );
+               $backend->dirty = false;
+
+               unset( $session[101] );
+               $this->assertFalse( $backend->dirty );
+       }
+
+       public function testClear() {
+               $session = TestUtils::getDummySession();
+               $priv = TestingAccessWrapper::newFromObject( $session );
+
+               $backend = $this->getMockBuilder( DummySessionBackend::class )
+                       ->setMethods( [ 'canSetUser', 'setUser', 'save' ] )
+                       ->getMock();
+               $backend->expects( $this->once() )->method( 'canSetUser' )
+                       ->will( $this->returnValue( true ) );
+               $backend->expects( $this->once() )->method( 'setUser' )
+                       ->with( $this->callback( function ( $user ) {
+                               return $user instanceof User && $user->isAnon();
+                       } ) );
+               $backend->expects( $this->once() )->method( 'save' );
+               $priv->backend = $backend;
+               $session->clear();
+               $this->assertSame( [], $backend->data );
+               $this->assertTrue( $backend->dirty );
+
+               $backend = $this->getMockBuilder( DummySessionBackend::class )
+                       ->setMethods( [ 'canSetUser', 'setUser', 'save' ] )
+                       ->getMock();
+               $backend->data = [];
+               $backend->expects( $this->once() )->method( 'canSetUser' )
+                       ->will( $this->returnValue( true ) );
+               $backend->expects( $this->once() )->method( 'setUser' )
+                       ->with( $this->callback( function ( $user ) {
+                               return $user instanceof User && $user->isAnon();
+                       } ) );
+               $backend->expects( $this->once() )->method( 'save' );
+               $priv->backend = $backend;
+               $session->clear();
+               $this->assertFalse( $backend->dirty );
+
+               $backend = $this->getMockBuilder( DummySessionBackend::class )
+                       ->setMethods( [ 'canSetUser', 'setUser', 'save' ] )
+                       ->getMock();
+               $backend->expects( $this->once() )->method( 'canSetUser' )
+                       ->will( $this->returnValue( false ) );
+               $backend->expects( $this->never() )->method( 'setUser' );
+               $backend->expects( $this->once() )->method( 'save' );
+               $priv->backend = $backend;
+               $session->clear();
+               $this->assertSame( [], $backend->data );
+               $this->assertTrue( $backend->dirty );
+       }
+
+       public function testTokens() {
+               $session = TestUtils::getDummySession();
+               $priv = TestingAccessWrapper::newFromObject( $session );
+               $backend = $priv->backend;
+
+               $token = TestingAccessWrapper::newFromObject( $session->getToken() );
+               $this->assertArrayHasKey( 'wsTokenSecrets', $backend->data );
+               $this->assertArrayHasKey( 'default', $backend->data['wsTokenSecrets'] );
+               $secret = $backend->data['wsTokenSecrets']['default'];
+               $this->assertSame( $secret, $token->secret );
+               $this->assertSame( '', $token->salt );
+               $this->assertTrue( $token->wasNew() );
+
+               $token = TestingAccessWrapper::newFromObject( $session->getToken( 'foo' ) );
+               $this->assertSame( $secret, $token->secret );
+               $this->assertSame( 'foo', $token->salt );
+               $this->assertFalse( $token->wasNew() );
+
+               $backend->data['wsTokenSecrets']['secret'] = 'sekret';
+               $token = TestingAccessWrapper::newFromObject(
+                       $session->getToken( [ 'bar', 'baz' ], 'secret' )
+               );
+               $this->assertSame( 'sekret', $token->secret );
+               $this->assertSame( 'bar|baz', $token->salt );
+               $this->assertFalse( $token->wasNew() );
+
+               $session->resetToken( 'secret' );
+               $this->assertArrayHasKey( 'wsTokenSecrets', $backend->data );
+               $this->assertArrayHasKey( 'default', $backend->data['wsTokenSecrets'] );
+               $this->assertArrayNotHasKey( 'secret', $backend->data['wsTokenSecrets'] );
+
+               $session->resetAllTokens();
+               $this->assertArrayNotHasKey( 'wsTokenSecrets', $backend->data );
+       }
+
+       /**
+        * @dataProvider provideSecretsRoundTripping
+        * @param mixed $data
+        */
+       public function testSecretsRoundTripping( $data ) {
+               $session = TestUtils::getDummySession();
+
+               // Simple round-trip
+               $session->setSecret( 'secret', $data );
+               $this->assertNotEquals( $data, $session->get( 'secret' ) );
+               $this->assertEquals( $data, $session->getSecret( 'secret', 'defaulted' ) );
+       }
+
+       public static function provideSecretsRoundTripping() {
+               return [
+                       [ 'Foobar' ],
+                       [ 42 ],
+                       [ [ 'foo', 'bar' => 'baz', 'subarray' => [ 1, 2, 3 ] ] ],
+                       [ (object)[ 'foo', 'bar' => 'baz', 'subarray' => [ 1, 2, 3 ] ] ],
+                       [ true ],
+                       [ false ],
+                       [ null ],
+               ];
+       }
+
+       public function testSecrets() {
+               $logger = new \TestLogger;
+               $session = TestUtils::getDummySession( null, -1, $logger );
+
+               // Simple defaulting
+               $this->assertEquals( 'defaulted', $session->getSecret( 'test', 'defaulted' ) );
+
+               // Bad encrypted data
+               $session->set( 'test', 'foobar' );
+               $logger->setCollect( true );
+               $this->assertEquals( 'defaulted', $session->getSecret( 'test', 'defaulted' ) );
+               $logger->setCollect( false );
+               $this->assertSame( [
+                       [ LogLevel::WARNING, 'Invalid sealed-secret format' ]
+               ], $logger->getBuffer() );
+               $logger->clearBuffer();
+
+               // Tampered data
+               $session->setSecret( 'test', 'foobar' );
+               $encrypted = $session->get( 'test' );
+               $session->set( 'test', $encrypted . 'x' );
+               $logger->setCollect( true );
+               $this->assertEquals( 'defaulted', $session->getSecret( 'test', 'defaulted' ) );
+               $logger->setCollect( false );
+               $this->assertSame( [
+                       [ LogLevel::WARNING, 'Sealed secret has been tampered with, aborting.' ]
+               ], $logger->getBuffer() );
+               $logger->clearBuffer();
+
+               // Unserializable data
+               $iv = random_bytes( 16 );
+               list( $encKey, $hmacKey ) = TestingAccessWrapper::newFromObject( $session )->getSecretKeys();
+               $ciphertext = openssl_encrypt( 'foobar', 'aes-256-ctr', $encKey, OPENSSL_RAW_DATA, $iv );
+               $sealed = base64_encode( $iv ) . '.' . base64_encode( $ciphertext );
+               $hmac = hash_hmac( 'sha256', $sealed, $hmacKey, true );
+               $encrypted = base64_encode( $hmac ) . '.' . $sealed;
+               $session->set( 'test', $encrypted );
+               \Wikimedia\suppressWarnings();
+               $this->assertEquals( 'defaulted', $session->getSecret( 'test', 'defaulted' ) );
+               \Wikimedia\restoreWarnings();
+       }
+
+}
diff --git a/tests/phpunit/unit/includes/session/TokenTest.php b/tests/phpunit/unit/includes/session/TokenTest.php
new file mode 100644 (file)
index 0000000..cab962b
--- /dev/null
@@ -0,0 +1,66 @@
+<?php
+
+namespace MediaWiki\Session;
+
+use Wikimedia\TestingAccessWrapper;
+
+/**
+ * @group Session
+ * @covers MediaWiki\Session\Token
+ */
+class TokenTest extends \MediaWikiUnitTestCase {
+
+       public function testBasics() {
+               $token = $this->getMockBuilder( Token::class )
+                       ->setMethods( [ 'toStringAtTimestamp' ] )
+                       ->setConstructorArgs( [ 'sekret', 'salty', true ] )
+                       ->getMock();
+               $token->expects( $this->any() )->method( 'toStringAtTimestamp' )
+                       ->will( $this->returnValue( 'faketoken+\\' ) );
+
+               $this->assertSame( 'faketoken+\\', $token->toString() );
+               $this->assertSame( 'faketoken+\\', (string)$token );
+               $this->assertTrue( $token->wasNew() );
+
+               $token = new Token( 'sekret', 'salty', false );
+               $this->assertFalse( $token->wasNew() );
+       }
+
+       public function testToStringAtTimestamp() {
+               $token = TestingAccessWrapper::newFromObject( new Token( 'sekret', 'salty', false ) );
+
+               $this->assertSame(
+                       'd9ade0c7d4349e9df9094e61c33a5a0d5644fde2+\\',
+                       $token->toStringAtTimestamp( 1447362018 )
+               );
+               $this->assertSame(
+                       'ee2f7a2488dea9176c224cfb400d43be5644fdea+\\',
+                       $token->toStringAtTimestamp( 1447362026 )
+               );
+       }
+
+       public function testGetTimestamp() {
+               $this->assertSame(
+                       1447362018, Token::getTimestamp( 'd9ade0c7d4349e9df9094e61c33a5a0d5644fde2+\\' )
+               );
+               $this->assertSame(
+                       1447362026, Token::getTimestamp( 'ee2f7a2488dea9176c224cfb400d43be5644fdea+\\' )
+               );
+               $this->assertNull( Token::getTimestamp( 'ee2f7a2488dea9176c224cfb400d43be5644fdea-\\' ) );
+               $this->assertNull( Token::getTimestamp( 'ee2f7a2488dea9176c224cfb400d43be+\\' ) );
+
+               $this->assertNull( Token::getTimestamp( 'ee2f7a2488dea9x76c224cfb400d43be5644fdea+\\' ) );
+       }
+
+       public function testMatch() {
+               $token = TestingAccessWrapper::newFromObject( new Token( 'sekret', 'salty', false ) );
+
+               $test = $token->toStringAtTimestamp( time() - 10 );
+               $this->assertTrue( $token->match( $test ) );
+               $this->assertTrue( $token->match( $test, 12 ) );
+               $this->assertFalse( $token->match( $test, 8 ) );
+
+               $this->assertFalse( $token->match( 'ee2f7a2488dea9176c224cfb400d43be5644fdea-\\' ) );
+       }
+
+}
diff --git a/tests/phpunit/unit/includes/shell/CommandFactoryTest.php b/tests/phpunit/unit/includes/shell/CommandFactoryTest.php
new file mode 100644 (file)
index 0000000..b031431
--- /dev/null
@@ -0,0 +1,50 @@
+<?php
+
+use MediaWiki\Shell\Command;
+use MediaWiki\Shell\CommandFactory;
+use MediaWiki\Shell\FirejailCommand;
+use Psr\Log\NullLogger;
+use Wikimedia\TestingAccessWrapper;
+
+/**
+ * @group Shell
+ */
+class CommandFactoryTest extends PHPUnit\Framework\TestCase {
+
+       use MediaWikiCoversValidator;
+
+       /**
+        * @covers MediaWiki\Shell\CommandFactory::create
+        */
+       public function testCreate() {
+               $logger = new NullLogger();
+               $cgroup = '/sys/fs/cgroup/memory/mygroup';
+               $limits = [
+                       'filesize' => 1000,
+                       'memory' => 1000,
+                       'time' => 30,
+                       'walltime' => 40,
+               ];
+
+               $factory = new CommandFactory( $limits, $cgroup, false );
+               $factory->setLogger( $logger );
+               $factory->logStderr();
+               $command = $factory->create();
+               $this->assertInstanceOf( Command::class, $command );
+
+               $wrapper = TestingAccessWrapper::newFromObject( $command );
+               $this->assertSame( $logger, $wrapper->logger );
+               $this->assertSame( $cgroup, $wrapper->cgroup );
+               $this->assertSame( $limits, $wrapper->limits );
+               $this->assertTrue( $wrapper->doLogStderr );
+       }
+
+       /**
+        * @covers MediaWiki\Shell\CommandFactory::create
+        */
+       public function testFirejailCreate() {
+               $factory = new CommandFactory( [], false, 'firejail' );
+               $factory->setLogger( new NullLogger() );
+               $this->assertInstanceOf( FirejailCommand::class, $factory->create() );
+       }
+}
diff --git a/tests/phpunit/unit/includes/shell/CommandTest.php b/tests/phpunit/unit/includes/shell/CommandTest.php
new file mode 100644 (file)
index 0000000..2e03163
--- /dev/null
@@ -0,0 +1,181 @@
+<?php
+
+use MediaWiki\Shell\Command;
+use Wikimedia\TestingAccessWrapper;
+
+/**
+ * @covers \MediaWiki\Shell\Command
+ * @group Shell
+ */
+class CommandTest extends PHPUnit\Framework\TestCase {
+
+       use MediaWikiCoversValidator;
+
+       private function requirePosix() {
+               if ( wfIsWindows() ) {
+                       $this->markTestSkipped( 'This test requires a POSIX environment.' );
+               }
+       }
+
+       /**
+        * @dataProvider provideExecute
+        */
+       public function testExecute( $commandInput, $expectedExitCode, $expectedOutput ) {
+               $this->requirePosix();
+
+               $command = new Command();
+               $result = $command
+                       ->params( $commandInput )
+                       ->execute();
+
+               $this->assertSame( $expectedExitCode, $result->getExitCode() );
+               $this->assertSame( $expectedOutput, $result->getStdout() );
+       }
+
+       public function provideExecute() {
+               return [
+                       'success status' => [ 'true', 0, '' ],
+                       'failure status' => [ 'false', 1, '' ],
+                       'output' => [ [ 'echo', '-n', 'x', '>', 'y' ], 0, 'x > y' ],
+               ];
+       }
+
+       public function testEnvironment() {
+               $this->requirePosix();
+
+               $command = new Command();
+               $result = $command
+                       ->params( [ 'printenv', 'FOO' ] )
+                       ->environment( [ 'FOO' => 'bar' ] )
+                       ->execute();
+               $this->assertSame( "bar\n", $result->getStdout() );
+       }
+
+       public function testStdout() {
+               $this->requirePosix();
+
+               $command = new Command();
+
+               $result = $command
+                       ->params( 'bash', '-c', 'echo ThisIsStderr 1>&2' )
+                       ->execute();
+
+               $this->assertNotContains( 'ThisIsStderr', $result->getStdout() );
+               $this->assertEquals( "ThisIsStderr\n", $result->getStderr() );
+       }
+
+       public function testStdoutRedirection() {
+               $this->requirePosix();
+
+               $command = new Command();
+
+               $result = $command
+                       ->params( 'bash', '-c', 'echo ThisIsStderr 1>&2' )
+                       ->includeStderr( true )
+                       ->execute();
+
+               $this->assertEquals( "ThisIsStderr\n", $result->getStdout() );
+               $this->assertNull( $result->getStderr() );
+       }
+
+       public function testOutput() {
+               global $IP;
+
+               $this->requirePosix();
+               chdir( $IP );
+
+               $command = new Command();
+               $result = $command
+                       ->params( [ 'ls', 'index.php' ] )
+                       ->execute();
+               $this->assertRegExp( '/^index.php$/m', $result->getStdout() );
+               $this->assertSame( null, $result->getStderr() );
+
+               $command = new Command();
+               $result = $command
+                       ->params( [ 'ls', 'index.php', 'no-such-file' ] )
+                       ->includeStderr()
+                       ->execute();
+               $this->assertRegExp( '/^index.php$/m', $result->getStdout() );
+               $this->assertRegExp( '/^.+no-such-file.*$/m', $result->getStdout() );
+               $this->assertSame( null, $result->getStderr() );
+
+               $command = new Command();
+               $result = $command
+                       ->params( [ 'ls', 'index.php', 'no-such-file' ] )
+                       ->execute();
+               $this->assertRegExp( '/^index.php$/m', $result->getStdout() );
+               $this->assertRegExp( '/^.+no-such-file.*$/m', $result->getStderr() );
+       }
+
+       /**
+        * Test that null values are skipped by params() and unsafeParams()
+        */
+       public function testNullsAreSkipped() {
+               $command = TestingAccessWrapper::newFromObject( new Command );
+               $command->params( 'echo', 'a', null, 'b' );
+               $command->unsafeParams( 'c', null, 'd' );
+               $this->assertEquals( "'echo' 'a' 'b' c d", $command->command );
+       }
+
+       public function testT69870() {
+               $commandLine = wfIsWindows()
+                       // 333 = 331 + CRLF
+                       ? ( 'for /l %i in (1, 1, 1001) do @echo ' . str_repeat( '*', 331 ) )
+                       : 'printf "%-333333s" "*"';
+
+               // Test several times because it involves a race condition that may randomly succeed or fail
+               for ( $i = 0; $i < 10; $i++ ) {
+                       $command = new Command();
+                       $output = $command->unsafeParams( $commandLine )
+                               ->execute()
+                               ->getStdout();
+                       $this->assertEquals( 333333, strlen( $output ) );
+               }
+       }
+
+       public function testLogStderr() {
+               $this->requirePosix();
+
+               $logger = new TestLogger( true, function ( $message, $level, $context ) {
+                       return $level === Psr\Log\LogLevel::ERROR ? '1' : null;
+               }, true );
+               $command = new Command();
+               $command->setLogger( $logger );
+               $command->params( 'bash', '-c', 'echo ThisIsStderr 1>&2' );
+               $command->execute();
+               $this->assertEmpty( $logger->getBuffer() );
+
+               $command = new Command();
+               $command->setLogger( $logger );
+               $command->logStderr();
+               $command->params( 'bash', '-c', 'echo ThisIsStderr 1>&2' );
+               $command->execute();
+               $this->assertSame( 1, count( $logger->getBuffer() ) );
+               $this->assertSame( trim( $logger->getBuffer()[0][2]['error'] ), 'ThisIsStderr' );
+       }
+
+       public function testInput() {
+               $this->requirePosix();
+
+               $command = new Command();
+               $command->params( 'cat' );
+               $command->input( 'abc' );
+               $result = $command->execute();
+               $this->assertSame( 'abc', $result->getStdout() );
+
+               // now try it with something that does not fit into a single block
+               $command = new Command();
+               $command->params( 'cat' );
+               $command->input( str_repeat( '!', 1000000 ) );
+               $result = $command->execute();
+               $this->assertSame( 1000000, strlen( $result->getStdout() ) );
+
+               // And try it with empty input
+               $command = new Command();
+               $command->params( 'cat' );
+               $command->input( '' );
+               $result = $command->execute();
+               $this->assertSame( '', $result->getStdout() );
+       }
+}
diff --git a/tests/phpunit/unit/includes/shell/FirejailCommandTest.php b/tests/phpunit/unit/includes/shell/FirejailCommandTest.php
new file mode 100644 (file)
index 0000000..b87271f
--- /dev/null
@@ -0,0 +1,86 @@
+<?php
+
+/**
+ * Copyright (C) 2017 Kunal Mehta <legoktm@member.fsf.org>
+ *
+ * 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; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * 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.
+ *
+ */
+
+use MediaWiki\Shell\FirejailCommand;
+use MediaWiki\Shell\Shell;
+use Wikimedia\TestingAccessWrapper;
+
+class FirejailCommandTest extends PHPUnit\Framework\TestCase {
+
+       use MediaWikiCoversValidator;
+
+       public function provideBuildFinalCommand() {
+               global $IP;
+               $basePath = realpath( $IP );
+               // phpcs:ignore Generic.Files.LineLength
+               $env = "'MW_INCLUDE_STDERR=;MW_CPU_LIMIT=180; MW_CGROUP='\'''\''; MW_MEM_LIMIT=307200; MW_FILE_SIZE_LIMIT=102400; MW_WALL_CLOCK_LIMIT=180; MW_USE_LOG_PIPE=yes'";
+               $limit = "/bin/bash '$basePath/includes/shell/limit.sh'";
+               $profile = "--profile=$basePath/includes/shell/firejail.profile";
+               $blacklist = '--blacklist=' . realpath( MW_CONFIG_FILE );
+               $default = "$blacklist --noroot --seccomp --private-dev";
+               return [
+                       [
+                               'No restrictions',
+                               'ls', 0, "$limit ''\''ls'\''' $env"
+                       ],
+                       [
+                               'default restriction',
+                               'ls', Shell::RESTRICT_DEFAULT,
+                               "$limit 'firejail --quiet $profile $default -- '\''ls'\''' $env"
+                       ],
+                       [
+                               'no network',
+                               'ls', Shell::NO_NETWORK,
+                               "$limit 'firejail --quiet $profile --net=none -- '\''ls'\''' $env"
+                       ],
+                       [
+                               'default restriction & no network',
+                               'ls', Shell::RESTRICT_DEFAULT | Shell::NO_NETWORK,
+                               "$limit 'firejail --quiet $profile $default --net=none -- '\''ls'\''' $env"
+                       ],
+                       [
+                               'seccomp',
+                               'ls', Shell::SECCOMP,
+                               "$limit 'firejail --quiet $profile --seccomp -- '\''ls'\''' $env"
+                       ],
+                       [
+                               'seccomp & no execve',
+                               'ls', Shell::SECCOMP | Shell::NO_EXECVE,
+                               "$limit 'firejail --quiet $profile --shell=none --seccomp=execve -- '\''ls'\''' $env"
+                       ],
+               ];
+       }
+
+       /**
+        * @covers \MediaWiki\Shell\FirejailCommand::buildFinalCommand()
+        * @dataProvider provideBuildFinalCommand
+        */
+       public function testBuildFinalCommand( $desc, $params, $flags, $expected ) {
+               $command = new FirejailCommand( 'firejail' );
+               $command
+                       ->params( $params )
+                       ->restrict( $flags );
+               $wrapper = TestingAccessWrapper::newFromObject( $command );
+               $output = $wrapper->buildFinalCommand( $wrapper->command );
+               $this->assertEquals( $expected, $output[0], $desc );
+       }
+
+}
diff --git a/tests/phpunit/unit/includes/site/CachingSiteStoreTest.php b/tests/phpunit/unit/includes/site/CachingSiteStoreTest.php
new file mode 100644 (file)
index 0000000..92ed1f5
--- /dev/null
@@ -0,0 +1,167 @@
+<?php
+
+/**
+ * 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; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * 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.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @since 1.25
+ *
+ * @ingroup Site
+ * @ingroup Test
+ *
+ * @group Site
+ * @group Database
+ *
+ * @author Jeroen De Dauw < jeroendedauw@gmail.com >
+ */
+class CachingSiteStoreTest extends \MediaWikiUnitTestCase {
+
+       /**
+        * @covers CachingSiteStore::getSites
+        */
+       public function testGetSites() {
+               $testSites = TestSites::getSites();
+
+               $store = new CachingSiteStore(
+                       $this->getHashSiteStore( $testSites ),
+                       ObjectCache::getLocalClusterInstance()
+               );
+
+               $sites = $store->getSites();
+
+               $this->assertInstanceOf( SiteList::class, $sites );
+
+               /**
+                * @var Site $site
+                */
+               foreach ( $sites as $site ) {
+                       $this->assertInstanceOf( Site::class, $site );
+               }
+
+               foreach ( $testSites as $site ) {
+                       if ( $site->getGlobalId() !== null ) {
+                               $this->assertTrue( $sites->hasSite( $site->getGlobalId() ) );
+                       }
+               }
+       }
+
+       /**
+        * @covers CachingSiteStore::saveSites
+        */
+       public function testSaveSites() {
+               $store = new CachingSiteStore(
+                       new HashSiteStore(), ObjectCache::getLocalClusterInstance()
+               );
+
+               $sites = [];
+
+               $site = new Site();
+               $site->setGlobalId( 'ertrywuutr' );
+               $site->setLanguageCode( 'en' );
+               $sites[] = $site;
+
+               $site = new MediaWikiSite();
+               $site->setGlobalId( 'sdfhxujgkfpth' );
+               $site->setLanguageCode( 'nl' );
+               $sites[] = $site;
+
+               $this->assertTrue( $store->saveSites( $sites ) );
+
+               $site = $store->getSite( 'ertrywuutr' );
+               $this->assertInstanceOf( Site::class, $site );
+               $this->assertEquals( 'en', $site->getLanguageCode() );
+
+               $site = $store->getSite( 'sdfhxujgkfpth' );
+               $this->assertInstanceOf( Site::class, $site );
+               $this->assertEquals( 'nl', $site->getLanguageCode() );
+       }
+
+       /**
+        * @covers CachingSiteStore::reset
+        */
+       public function testReset() {
+               $dbSiteStore = $this->getMockBuilder( SiteStore::class )
+                       ->disableOriginalConstructor()
+                       ->getMock();
+
+               $dbSiteStore->expects( $this->any() )
+                       ->method( 'getSite' )
+                       ->will( $this->returnValue( $this->getTestSite() ) );
+
+               $dbSiteStore->expects( $this->any() )
+                       ->method( 'getSites' )
+                       ->will( $this->returnCallback( function () {
+                               $siteList = new SiteList();
+                               $siteList->setSite( $this->getTestSite() );
+
+                               return $siteList;
+                       } ) );
+
+               $store = new CachingSiteStore( $dbSiteStore, ObjectCache::getLocalClusterInstance() );
+
+               // initialize internal cache
+               $this->assertGreaterThan( 0, $store->getSites()->count(), 'count sites' );
+
+               $store->getSite( 'enwiki' )->setLanguageCode( 'en-ca' );
+
+               // sanity check: $store should have the new language code for 'enwiki'
+               $this->assertEquals( 'en-ca', $store->getSite( 'enwiki' )->getLanguageCode(), 'sanity check' );
+
+               // purge cache
+               $store->reset();
+
+               // the internal cache of $store should be updated, and now pulling
+               // the site from the 'fallback' DBSiteStore with the original language code.
+               $this->assertEquals( 'en', $store->getSite( 'enwiki' )->getLanguageCode(), 'reset' );
+       }
+
+       public function getTestSite() {
+               $enwiki = new MediaWikiSite();
+               $enwiki->setGlobalId( 'enwiki' );
+               $enwiki->setLanguageCode( 'en' );
+
+               return $enwiki;
+       }
+
+       /**
+        * @covers CachingSiteStore::clear
+        */
+       public function testClear() {
+               $store = new CachingSiteStore(
+                       new HashSiteStore(), ObjectCache::getLocalClusterInstance()
+               );
+               $this->assertTrue( $store->clear() );
+
+               $site = $store->getSite( 'enwiki' );
+               $this->assertNull( $site );
+
+               $sites = $store->getSites();
+               $this->assertEquals( 0, $sites->count() );
+       }
+
+       /**
+        * @param Site[] $sites
+        *
+        * @return SiteStore
+        */
+       private function getHashSiteStore( array $sites ) {
+               $siteStore = new HashSiteStore();
+               $siteStore->saveSites( $sites );
+
+               return $siteStore;
+       }
+
+}
diff --git a/tests/phpunit/unit/includes/site/HashSiteStoreTest.php b/tests/phpunit/unit/includes/site/HashSiteStoreTest.php
new file mode 100644 (file)
index 0000000..8b0d4e0
--- /dev/null
@@ -0,0 +1,105 @@
+<?php
+
+/**
+ * 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; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * 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.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @since 1.25
+ *
+ * @ingroup Site
+ * @group Site
+ *
+ * @author Katie Filbert < aude.wiki@gmail.com >
+ */
+class HashSiteStoreTest extends \MediaWikiUnitTestCase {
+
+       /**
+        * @covers HashSiteStore::getSites
+        */
+       public function testGetSites() {
+               $expectedSites = [];
+
+               foreach ( TestSites::getSites() as $testSite ) {
+                       $siteId = $testSite->getGlobalId();
+                       $expectedSites[$siteId] = $testSite;
+               }
+
+               $siteStore = new HashSiteStore( $expectedSites );
+
+               $this->assertEquals( new SiteList( $expectedSites ), $siteStore->getSites() );
+       }
+
+       /**
+        * @covers HashSiteStore::saveSite
+        * @covers HashSiteStore::getSite
+        */
+       public function testSaveSite() {
+               $store = new HashSiteStore();
+
+               $site = new Site();
+               $site->setGlobalId( 'dewiki' );
+
+               $this->assertCount( 0, $store->getSites(), '0 sites in store' );
+
+               $store->saveSite( $site );
+
+               $this->assertCount( 1, $store->getSites(), 'Store has 1 sites' );
+               $this->assertEquals( $site, $store->getSite( 'dewiki' ), 'Store has dewiki' );
+       }
+
+       /**
+        * @covers HashSiteStore::saveSites
+        */
+       public function testSaveSites() {
+               $store = new HashSiteStore();
+
+               $sites = [];
+
+               $site = new Site();
+               $site->setGlobalId( 'enwiki' );
+               $site->setLanguageCode( 'en' );
+               $sites[] = $site;
+
+               $site = new MediaWikiSite();
+               $site->setGlobalId( 'eswiki' );
+               $site->setLanguageCode( 'es' );
+               $sites[] = $site;
+
+               $this->assertCount( 0, $store->getSites(), '0 sites in store' );
+
+               $store->saveSites( $sites );
+
+               $this->assertCount( 2, $store->getSites(), 'Store has 2 sites' );
+               $this->assertTrue( $store->getSites()->hasSite( 'enwiki' ), 'Store has enwiki' );
+               $this->assertTrue( $store->getSites()->hasSite( 'eswiki' ), 'Store has eswiki' );
+       }
+
+       /**
+        * @covers HashSiteStore::clear
+        */
+       public function testClear() {
+               $store = new HashSiteStore();
+
+               $site = new Site();
+               $site->setGlobalId( 'arwiki' );
+               $store->saveSite( $site );
+
+               $this->assertCount( 1, $store->getSites(), '1 site in store' );
+
+               $store->clear();
+               $this->assertCount( 0, $store->getSites(), '0 sites in store' );
+       }
+}
diff --git a/tests/phpunit/unit/includes/site/MediaWikiPageNameNormalizerTest.php b/tests/phpunit/unit/includes/site/MediaWikiPageNameNormalizerTest.php
new file mode 100644 (file)
index 0000000..15894a3
--- /dev/null
@@ -0,0 +1,116 @@
+<?php
+
+use MediaWiki\Site\MediaWikiPageNameNormalizer;
+
+/**
+ * @covers MediaWiki\Site\MediaWikiPageNameNormalizer
+ *
+ * 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; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * 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.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @since 1.27
+ *
+ * @group Site
+ * @group medium
+ *
+ * @author Marius Hoch
+ */
+class MediaWikiPageNameNormalizerTest extends PHPUnit\Framework\TestCase {
+
+       use MediaWikiCoversValidator;
+
+       /**
+        * @dataProvider normalizePageTitleProvider
+        */
+       public function testNormalizePageTitle( $expected, $pageName, $getResponse ) {
+               MediaWikiPageNameNormalizerTestMockHttp::$response = $getResponse;
+
+               $normalizer = new MediaWikiPageNameNormalizer(
+                       new MediaWikiPageNameNormalizerTestMockHttp()
+               );
+
+               $this->assertSame(
+                       $expected,
+                       $normalizer->normalizePageName( $pageName, 'https://www.wikidata.org/w/api.php' )
+               );
+       }
+
+       public function normalizePageTitleProvider() {
+               // Response are taken from wikidata and kkwiki using the following API request
+               // api.php?action=query&prop=info&redirects=1&converttitles=1&format=json&titles=…
+               return [
+                       'universe (Q1)' => [
+                               'Q1',
+                               'Q1',
+                               '{"batchcomplete":"","query":{"pages":{"129":{"pageid":129,"ns":0,'
+                               . '"title":"Q1","contentmodel":"wikibase-item","pagelanguage":"en",'
+                               . '"pagelanguagehtmlcode":"en","pagelanguagedir":"ltr",'
+                               . '"touched":"2016-06-23T05:11:21Z","lastrevid":350004448,"length":58001}}}}'
+                       ],
+                       'Q404 redirects to Q395' => [
+                               'Q395',
+                               'Q404',
+                               '{"batchcomplete":"","query":{"redirects":[{"from":"Q404","to":"Q395"}],"pages"'
+                               . ':{"601":{"pageid":601,"ns":0,"title":"Q395","contentmodel":"wikibase-item",'
+                               . '"pagelanguage":"en","pagelanguagehtmlcode":"en","pagelanguagedir":"ltr",'
+                               . '"touched":"2016-06-23T08:00:20Z","lastrevid":350021914,"length":60108}}}}'
+                       ],
+                       'D converted to Д (Latin to Cyrillic) (taken from kkwiki)' => [
+                               'Д',
+                               'D',
+                               '{"batchcomplete":"","query":{"converted":[{"from":"D","to":"\u0414"}],'
+                               . '"pages":{"510541":{"pageid":510541,"ns":0,"title":"\u0414",'
+                               . '"contentmodel":"wikitext","pagelanguage":"kk","pagelanguagehtmlcode":"kk",'
+                               . '"pagelanguagedir":"ltr","touched":"2015-11-22T09:16:18Z",'
+                               . '"lastrevid":2373618,"length":3501}}}}'
+                       ],
+                       'there is no Q0' => [
+                               false,
+                               'Q0',
+                               '{"batchcomplete":"","query":{"pages":{"-1":{"ns":0,"title":"Q0",'
+                               . '"missing":"","contentmodel":"wikibase-item","pagelanguage":"en",'
+                               . '"pagelanguagehtmlcode":"en","pagelanguagedir":"ltr"}}}}'
+                       ],
+                       'invalid title' => [
+                               false,
+                               '{{',
+                               '{"batchcomplete":"","query":{"pages":{"-1":{"title":"{{",'
+                               . '"invalidreason":"The requested page title contains invalid '
+                               . 'characters: \"{\".","invalid":""}}}}'
+                       ],
+                       'error on get' => [ false, 'ABC', false ]
+               ];
+       }
+
+}
+
+/**
+ * @private
+ * @see Http
+ */
+class MediaWikiPageNameNormalizerTestMockHttp extends Http {
+
+       /**
+        * @var mixed
+        */
+       public static $response;
+
+       public static function get( $url, array $options = [], $caller = __METHOD__ ) {
+               PHPUnit_Framework_Assert::assertInternalType( 'string', $url );
+               PHPUnit_Framework_Assert::assertInternalType( 'string', $caller );
+
+               return self::$response;
+       }
+}
diff --git a/tests/phpunit/unit/includes/site/SiteExporterTest.php b/tests/phpunit/unit/includes/site/SiteExporterTest.php
new file mode 100644 (file)
index 0000000..707be45
--- /dev/null
@@ -0,0 +1,148 @@
+<?php
+
+/**
+ * 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; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * 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.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ *
+ * @ingroup Site
+ * @ingroup Test
+ *
+ * @group Site
+ *
+ * @covers SiteExporter
+ *
+ * @author Daniel Kinzler
+ */
+class SiteExporterTest extends PHPUnit\Framework\TestCase {
+
+       use MediaWikiCoversValidator;
+       use PHPUnit4And6Compat;
+
+       public function testConstructor_InvalidArgument() {
+               $this->setExpectedException( InvalidArgumentException::class );
+
+               new SiteExporter( 'Foo' );
+       }
+
+       public function testExportSites() {
+               $foo = Site::newForType( Site::TYPE_UNKNOWN );
+               $foo->setGlobalId( 'Foo' );
+
+               $acme = Site::newForType( Site::TYPE_UNKNOWN );
+               $acme->setGlobalId( 'acme.com' );
+               $acme->setGroup( 'Test' );
+               $acme->addLocalId( Site::ID_INTERWIKI, 'acme' );
+               $acme->setPath( Site::PATH_LINK, 'http://acme.com/' );
+
+               $tmp = tmpfile();
+               $exporter = new SiteExporter( $tmp );
+
+               $exporter->exportSites( [ $foo, $acme ] );
+
+               fseek( $tmp, 0 );
+               $xml = fread( $tmp, 16 * 1024 );
+
+               $this->assertContains( '<sites ', $xml );
+               $this->assertContains( '<site>', $xml );
+               $this->assertContains( '<globalid>Foo</globalid>', $xml );
+               $this->assertContains( '</site>', $xml );
+               $this->assertContains( '<globalid>acme.com</globalid>', $xml );
+               $this->assertContains( '<group>Test</group>', $xml );
+               $this->assertContains( '<localid type="interwiki">acme</localid>', $xml );
+               $this->assertContains( '<path type="link">http://acme.com/</path>', $xml );
+               $this->assertContains( '</sites>', $xml );
+
+               // NOTE: HHVM (at least on wmf Jenkins) doesn't like file URLs.
+               $xsdFile = __DIR__ . '/../../../../../docs/sitelist-1.0.xsd';
+               $xsdData = file_get_contents( $xsdFile );
+
+               $document = new DOMDocument();
+               $document->loadXML( $xml, LIBXML_NONET );
+               $document->schemaValidateSource( $xsdData );
+       }
+
+       private function newSiteStore( SiteList $sites ) {
+               $store = $this->getMockBuilder( SiteStore::class )->getMock();
+
+               $store->expects( $this->once() )
+                       ->method( 'saveSites' )
+                       ->will( $this->returnCallback( function ( $moreSites ) use ( $sites ) {
+                               foreach ( $moreSites as $site ) {
+                                       $sites->setSite( $site );
+                               }
+                       } ) );
+
+               $store->expects( $this->any() )
+                       ->method( 'getSites' )
+                       ->will( $this->returnValue( new SiteList() ) );
+
+               return $store;
+       }
+
+       public function provideRoundTrip() {
+               $foo = Site::newForType( Site::TYPE_UNKNOWN );
+               $foo->setGlobalId( 'Foo' );
+
+               $acme = Site::newForType( Site::TYPE_UNKNOWN );
+               $acme->setGlobalId( 'acme.com' );
+               $acme->setGroup( 'Test' );
+               $acme->addLocalId( Site::ID_INTERWIKI, 'acme' );
+               $acme->setPath( Site::PATH_LINK, 'http://acme.com/' );
+
+               $dewiki = Site::newForType( Site::TYPE_MEDIAWIKI );
+               $dewiki->setGlobalId( 'dewiki' );
+               $dewiki->setGroup( 'wikipedia' );
+               $dewiki->setForward( true );
+               $dewiki->addLocalId( Site::ID_INTERWIKI, 'wikipedia' );
+               $dewiki->addLocalId( Site::ID_EQUIVALENT, 'de' );
+               $dewiki->setPath( Site::PATH_LINK, 'http://de.wikipedia.org/w/' );
+               $dewiki->setPath( MediaWikiSite::PATH_PAGE, 'http://de.wikipedia.org/wiki/' );
+               $dewiki->setSource( 'meta.wikimedia.org' );
+
+               return [
+                       'empty' => [
+                               new SiteList()
+                       ],
+
+                       'some' => [
+                               new SiteList( [ $foo, $acme, $dewiki ] ),
+                       ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideRoundTrip()
+        */
+       public function testRoundTrip( SiteList $sites ) {
+               $tmp = tmpfile();
+               $exporter = new SiteExporter( $tmp );
+
+               $exporter->exportSites( $sites );
+
+               fseek( $tmp, 0 );
+               $xml = fread( $tmp, 16 * 1024 );
+
+               $actualSites = new SiteList();
+               $store = $this->newSiteStore( $actualSites );
+
+               $importer = new SiteImporter( $store );
+               $importer->importFromXML( $xml );
+
+               $this->assertEquals( $sites, $actualSites );
+       }
+
+}
diff --git a/tests/phpunit/unit/includes/site/SiteImporterTest.php b/tests/phpunit/unit/includes/site/SiteImporterTest.php
new file mode 100644 (file)
index 0000000..dbdbd6f
--- /dev/null
@@ -0,0 +1,200 @@
+<?php
+
+/**
+ * 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; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * 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.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ *
+ * @ingroup Site
+ * @ingroup Test
+ *
+ * @group Site
+ *
+ * @covers SiteImporter
+ *
+ * @author Daniel Kinzler
+ */
+class SiteImporterTest extends PHPUnit\Framework\TestCase {
+
+       use MediaWikiCoversValidator;
+       use PHPUnit4And6Compat;
+
+       private function newSiteImporter( array $expectedSites, $errorCount ) {
+               $store = $this->getMockBuilder( SiteStore::class )->getMock();
+
+               $store->expects( $this->once() )
+                       ->method( 'saveSites' )
+                       ->will( $this->returnCallback( function ( $sites ) use ( $expectedSites ) {
+                               $this->assertSitesEqual( $expectedSites, $sites );
+                       } ) );
+
+               $store->expects( $this->any() )
+                       ->method( 'getSites' )
+                       ->will( $this->returnValue( new SiteList() ) );
+
+               $errorHandler = $this->getMockBuilder( Psr\Log\LoggerInterface::class )->getMock();
+               $errorHandler->expects( $this->exactly( $errorCount ) )
+                       ->method( 'error' );
+
+               $importer = new SiteImporter( $store );
+               $importer->setExceptionCallback( [ $errorHandler, 'error' ] );
+
+               return $importer;
+       }
+
+       public function assertSitesEqual( $expected, $actual, $message = '' ) {
+               $this->assertEquals(
+                       $this->getSerializedSiteList( $expected ),
+                       $this->getSerializedSiteList( $actual ),
+                       $message
+               );
+       }
+
+       public function provideImportFromXML() {
+               $foo = Site::newForType( Site::TYPE_UNKNOWN );
+               $foo->setGlobalId( 'Foo' );
+
+               $acme = Site::newForType( Site::TYPE_UNKNOWN );
+               $acme->setGlobalId( 'acme.com' );
+               $acme->setGroup( 'Test' );
+               $acme->addLocalId( Site::ID_INTERWIKI, 'acme' );
+               $acme->setPath( Site::PATH_LINK, 'http://acme.com/' );
+
+               $dewiki = Site::newForType( Site::TYPE_MEDIAWIKI );
+               $dewiki->setGlobalId( 'dewiki' );
+               $dewiki->setGroup( 'wikipedia' );
+               $dewiki->setForward( true );
+               $dewiki->addLocalId( Site::ID_INTERWIKI, 'wikipedia' );
+               $dewiki->addLocalId( Site::ID_EQUIVALENT, 'de' );
+               $dewiki->setPath( Site::PATH_LINK, 'http://de.wikipedia.org/w/' );
+               $dewiki->setPath( MediaWikiSite::PATH_PAGE, 'http://de.wikipedia.org/wiki/' );
+               $dewiki->setSource( 'meta.wikimedia.org' );
+
+               return [
+                       'empty' => [
+                               '<sites></sites>',
+                               [],
+                       ],
+                       'no sites' => [
+                               '<sites><Foo><globalid>Foo</globalid></Foo><Bar><quux>Bla</quux></Bar></sites>',
+                               [],
+                       ],
+                       'minimal' => [
+                               '<sites>' .
+                                       '<site><globalid>Foo</globalid></site>' .
+                               '</sites>',
+                               [ $foo ],
+                       ],
+                       'full' => [
+                               '<sites>' .
+                                       '<site><globalid>Foo</globalid></site>' .
+                                       '<site>' .
+                                               '<globalid>acme.com</globalid>' .
+                                               '<localid type="interwiki">acme</localid>' .
+                                               '<group>Test</group>' .
+                                               '<path type="link">http://acme.com/</path>' .
+                                       '</site>' .
+                                       '<site type="mediawiki">' .
+                                               '<source>meta.wikimedia.org</source>' .
+                                               '<globalid>dewiki</globalid>' .
+                                               '<localid type="interwiki">wikipedia</localid>' .
+                                               '<localid type="equivalent">de</localid>' .
+                                               '<group>wikipedia</group>' .
+                                               '<forward/>' .
+                                               '<path type="link">http://de.wikipedia.org/w/</path>' .
+                                               '<path type="page_path">http://de.wikipedia.org/wiki/</path>' .
+                                       '</site>' .
+                               '</sites>',
+                               [ $foo, $acme, $dewiki ],
+                       ],
+                       'skip' => [
+                               '<sites>' .
+                                       '<site><globalid>Foo</globalid></site>' .
+                                       '<site><barf>Foo</barf></site>' .
+                                       '<site>' .
+                                               '<globalid>acme.com</globalid>' .
+                                               '<localid type="interwiki">acme</localid>' .
+                                               '<silly>boop!</silly>' .
+                                               '<group>Test</group>' .
+                                               '<path type="link">http://acme.com/</path>' .
+                                       '</site>' .
+                               '</sites>',
+                               [ $foo, $acme ],
+                               1
+                       ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideImportFromXML
+        */
+       public function testImportFromXML( $xml, array $expectedSites, $errorCount = 0 ) {
+               $importer = $this->newSiteImporter( $expectedSites, $errorCount );
+               $importer->importFromXML( $xml );
+       }
+
+       public function testImportFromXML_malformed() {
+               $this->setExpectedException( Exception::class );
+
+               $store = $this->getMockBuilder( SiteStore::class )->getMock();
+               $importer = new SiteImporter( $store );
+               $importer->importFromXML( 'THIS IS NOT XML' );
+       }
+
+       public function testImportFromFile() {
+               $foo = Site::newForType( Site::TYPE_UNKNOWN );
+               $foo->setGlobalId( 'Foo' );
+
+               $acme = Site::newForType( Site::TYPE_UNKNOWN );
+               $acme->setGlobalId( 'acme.com' );
+               $acme->setGroup( 'Test' );
+               $acme->addLocalId( Site::ID_INTERWIKI, 'acme' );
+               $acme->setPath( Site::PATH_LINK, 'http://acme.com/' );
+
+               $dewiki = Site::newForType( Site::TYPE_MEDIAWIKI );
+               $dewiki->setGlobalId( 'dewiki' );
+               $dewiki->setGroup( 'wikipedia' );
+               $dewiki->setForward( true );
+               $dewiki->addLocalId( Site::ID_INTERWIKI, 'wikipedia' );
+               $dewiki->addLocalId( Site::ID_EQUIVALENT, 'de' );
+               $dewiki->setPath( Site::PATH_LINK, 'http://de.wikipedia.org/w/' );
+               $dewiki->setPath( MediaWikiSite::PATH_PAGE, 'http://de.wikipedia.org/wiki/' );
+               $dewiki->setSource( 'meta.wikimedia.org' );
+
+               $importer = $this->newSiteImporter( [ $foo, $acme, $dewiki ], 0 );
+
+               $file = __DIR__ . '/SiteImporterTest.xml';
+               $importer->importFromFile( $file );
+       }
+
+       /**
+        * @param Site[] $sites
+        *
+        * @return array[]
+        */
+       private function getSerializedSiteList( $sites ) {
+               $serialized = [];
+
+               foreach ( $sites as $site ) {
+                       $key = $site->getGlobalId();
+                       $data = unserialize( $site->serialize() );
+
+                       $serialized[$key] = $data;
+               }
+
+               return $serialized;
+       }
+}
diff --git a/tests/phpunit/unit/includes/site/SiteImporterTest.xml b/tests/phpunit/unit/includes/site/SiteImporterTest.xml
new file mode 100644 (file)
index 0000000..720b1fa
--- /dev/null
@@ -0,0 +1,19 @@
+<sites version="1.0" xmlns="http://www.mediawiki.org/xml/sitelist-1.0/">
+       <site><globalid>Foo</globalid></site>
+       <site>
+               <globalid>acme.com</globalid>
+               <localid type="interwiki">acme</localid>
+               <group>Test</group>
+               <path type="link">http://acme.com/</path>
+       </site>
+       <site type="mediawiki">
+               <source>meta.wikimedia.org</source>
+               <globalid>dewiki</globalid>
+               <localid type="interwiki">wikipedia</localid>
+               <localid type="equivalent">de</localid>
+               <group>wikipedia</group>
+               <forward/>
+               <path type="link">http://de.wikipedia.org/w/</path>
+               <path type="page_path">http://de.wikipedia.org/wiki/</path>
+       </site>
+</sites>
diff --git a/tests/phpunit/unit/includes/skins/SkinFactoryTest.php b/tests/phpunit/unit/includes/skins/SkinFactoryTest.php
new file mode 100644 (file)
index 0000000..8443c8d
--- /dev/null
@@ -0,0 +1,82 @@
+<?php
+
+class SkinFactoryTest extends \MediaWikiUnitTestCase {
+
+       /**
+        * @covers SkinFactory::register
+        */
+       public function testRegister() {
+               $factory = new SkinFactory();
+               $factory->register( 'fallback', 'Fallback', function () {
+                       return new SkinFallback();
+               } );
+               $this->assertTrue( true ); // No exception thrown
+               $this->setExpectedException( InvalidArgumentException::class );
+               $factory->register( 'invalid', 'Invalid', 'Invalid callback' );
+       }
+
+       /**
+        * @covers SkinFactory::makeSkin
+        */
+       public function testMakeSkinWithNoBuilders() {
+               $factory = new SkinFactory();
+               $this->setExpectedException( SkinException::class );
+               $factory->makeSkin( 'nobuilderregistered' );
+       }
+
+       /**
+        * @covers SkinFactory::makeSkin
+        */
+       public function testMakeSkinWithInvalidCallback() {
+               $factory = new SkinFactory();
+               $factory->register( 'unittest', 'Unittest', function () {
+                       return true; // Not a Skin object
+               } );
+               $this->setExpectedException( UnexpectedValueException::class );
+               $factory->makeSkin( 'unittest' );
+       }
+
+       /**
+        * @covers SkinFactory::makeSkin
+        */
+       public function testMakeSkinWithValidCallback() {
+               $factory = new SkinFactory();
+               $factory->register( 'testfallback', 'TestFallback', function () {
+                       return new SkinFallback();
+               } );
+
+               $skin = $factory->makeSkin( 'testfallback' );
+               $this->assertInstanceOf( Skin::class, $skin );
+               $this->assertInstanceOf( SkinFallback::class, $skin );
+               $this->assertEquals( 'fallback', $skin->getSkinName() );
+       }
+
+       /**
+        * @covers Skin::__construct
+        * @covers Skin::getSkinName
+        */
+       public function testGetSkinName() {
+               $skin = new SkinFallback();
+               $this->assertEquals( 'fallback', $skin->getSkinName(), 'Default' );
+               $skin = new SkinFallback( 'testname' );
+               $this->assertEquals( 'testname', $skin->getSkinName(), 'Constructor argument' );
+       }
+
+       /**
+        * @covers SkinFactory::getSkinNames
+        */
+       public function testGetSkinNames() {
+               $factory = new SkinFactory();
+               // A fake callback we can use that will never be called
+               $callback = function () {
+                       // NOP
+               };
+               $factory->register( 'skin1', 'Skin1', $callback );
+               $factory->register( 'skin2', 'Skin2', $callback );
+               $names = $factory->getSkinNames();
+               $this->assertArrayHasKey( 'skin1', $names );
+               $this->assertArrayHasKey( 'skin2', $names );
+               $this->assertEquals( 'Skin1', $names['skin1'] );
+               $this->assertEquals( 'Skin2', $names['skin2'] );
+       }
+}
diff --git a/tests/phpunit/unit/includes/skins/SkinTemplateTest.php b/tests/phpunit/unit/includes/skins/SkinTemplateTest.php
new file mode 100644 (file)
index 0000000..ec0c9c7
--- /dev/null
@@ -0,0 +1,109 @@
+<?php
+
+/**
+ * @covers SkinTemplate
+ *
+ * @group Output
+ *
+ * @author Bene* < benestar.wikimedia@gmail.com >
+ */
+class SkinTemplateTest extends \MediaWikiUnitTestCase {
+       /**
+        * @dataProvider makeListItemProvider
+        */
+       public function testMakeListItem( $expected, $key, $item, $options, $message ) {
+               $template = $this->getMockForAbstractClass( BaseTemplate::class );
+
+               $this->assertEquals(
+                       $expected,
+                       $template->makeListItem( $key, $item, $options ),
+                       $message
+               );
+       }
+
+       public function makeListItemProvider() {
+               return [
+                       [
+                               '<li class="class" title="itemtitle"><a href="url" title="title">text</a></li>',
+                               '',
+                               [
+                                       'class' => 'class',
+                                       'itemtitle' => 'itemtitle',
+                                       'href' => 'url',
+                                       'title' => 'title',
+                                       'text' => 'text'
+                               ],
+                               [],
+                               'Test makeListItem with normal values'
+                       ]
+               ];
+       }
+
+       /**
+        * @return PHPUnit_Framework_MockObject_MockObject|OutputPage
+        */
+       private function getMockOutputPage( $isSyndicated, $html ) {
+               $mock = $this->getMockBuilder( OutputPage::class )
+                       ->disableOriginalConstructor()
+                       ->getMock();
+               $mock->expects( $this->once() )
+                       ->method( 'isSyndicated' )
+                       ->will( $this->returnValue( $isSyndicated ) );
+               $mock->expects( $this->any() )
+                       ->method( 'getHTML' )
+                       ->will( $this->returnValue( $html ) );
+               return $mock;
+       }
+
+       public function provideGetDefaultModules() {
+               $defaultStyles = [
+                       'mediawiki.legacy.shared',
+                       'mediawiki.legacy.commonPrint',
+               ];
+               $buttonStyle = 'mediawiki.ui.button';
+               $feedStyle = 'mediawiki.feedlink';
+               return [
+                       [
+                               false,
+                               '',
+                               $defaultStyles
+                       ],
+                       [
+                               true,
+                               '',
+                               array_merge( $defaultStyles, [ $feedStyle ] )
+                       ],
+                       [
+                               false,
+                               'FOO mw-ui-button BAR',
+                               array_merge( $defaultStyles, [ $buttonStyle ] )
+                       ],
+                       [
+                               true,
+                               'FOO mw-ui-button BAR',
+                               array_merge( $defaultStyles, [ $buttonStyle, $feedStyle ] )
+                       ],
+               ];
+       }
+
+       /**
+        * @covers Skin::getDefaultModules
+        * @dataProvider provideGetDefaultModules
+        */
+       public function testgetDefaultModules( $isSyndicated, $html, $expectedModuleStyles ) {
+               $skin = new SkinTemplate();
+
+               $context = new DerivativeContext( $skin->getContext() );
+               $context->setOutput( $this->getMockOutputPage( $isSyndicated, $html ) );
+               $skin->setContext( $context );
+
+               $modules = $skin->getDefaultModules();
+
+               $actualStylesModule = call_user_func_array( 'array_merge', $modules['styles'] );
+               $this->assertArraySubset(
+                       $expectedModuleStyles,
+                       $actualStylesModule,
+                       'style modules'
+               );
+       }
+}
diff --git a/tests/phpunit/unit/includes/skins/SkinTest.php b/tests/phpunit/unit/includes/skins/SkinTest.php
new file mode 100644 (file)
index 0000000..da42437
--- /dev/null
@@ -0,0 +1,16 @@
+<?php
+class SkinTest extends \MediaWikiUnitTestCase {
+
+       /**
+        * @covers Skin::getDefaultModules
+        */
+       public function testGetDefaultModules() {
+               $skin = $this->getMockBuilder( Skin::class )
+                       ->setMethods( [ 'outputPage', 'setupSkinUserCss' ] )
+                       ->getMock();
+
+               $modules = $skin->getDefaultModules();
+               $this->assertTrue( isset( $modules['core'] ), 'core key is set by default' );
+               $this->assertTrue( isset( $modules['styles'] ), 'style key is set by default' );
+       }
+}
diff --git a/tests/phpunit/unit/includes/sparql/SparqlClientTest.php b/tests/phpunit/unit/includes/sparql/SparqlClientTest.php
new file mode 100644 (file)
index 0000000..62af489
--- /dev/null
@@ -0,0 +1,191 @@
+<?php
+
+namespace MediaWiki\Sparql;
+
+use Http;
+use MediaWiki\Http\HttpRequestFactory;
+use MWHttpRequest;
+use PHPUnit4And6Compat;
+
+/**
+ * @covers \MediaWiki\Sparql\SparqlClient
+ */
+class SparqlClientTest extends \PHPUnit\Framework\TestCase {
+
+       use PHPUnit4And6Compat;
+
+       private function getRequestFactory( $request ) {
+               $requestFactory = $this->getMock( HttpRequestFactory::class );
+               $requestFactory->method( 'create' )->willReturn( $request );
+               return $requestFactory;
+       }
+
+       private function getRequestMock( $content ) {
+               $request = $this->getMockBuilder( MWHttpRequest::class )->disableOriginalConstructor()->getMock();
+               $request->method( 'execute' )->willReturn( \Status::newGood( 200 ) );
+               $request->method( 'getContent' )->willReturn( $content );
+               return $request;
+       }
+
+       public function testQuery() {
+               $json = <<<JSON
+{
+  "head" : {
+    "vars" : [ "x", "y", "z" ]
+  },
+  "results" : {
+    "bindings" : [ {
+      "x" : {
+        "type" : "uri",
+        "value" : "http://wikiba.se/ontology#Dump"
+      },
+      "y" : {
+        "type" : "uri",
+        "value" : "http://creativecommons.org/ns#license"
+      },
+      "z" : {
+        "type" : "uri",
+        "value" : "http://creativecommons.org/publicdomain/zero/1.0/"
+      }
+    }, {
+      "x" : {
+        "type" : "uri",
+        "value" : "http://wikiba.se/ontology#Dump"
+      },
+      "z" : {
+        "type" : "literal",
+        "value" : "0.1.0"
+      }
+    } ]
+  }
+}
+JSON;
+
+               $request = $this->getRequestMock( $json );
+               $client = new SparqlClient( 'http://acme.test/', $this->getRequestFactory( $request ) );
+
+               // values only
+               $result = $client->query( "TEST SPARQL" );
+               $this->assertCount( 2, $result );
+               $this->assertEquals( 'http://wikiba.se/ontology#Dump', $result[0]['x'] );
+               $this->assertEquals( 'http://creativecommons.org/ns#license', $result[0]['y'] );
+               $this->assertEquals( '0.1.0', $result[1]['z'] );
+               $this->assertNull( $result[1]['y'] );
+               // raw data format
+               $result = $client->query( "TEST SPARQL 2", true );
+               $this->assertCount( 2, $result );
+               $this->assertEquals( 'uri', $result[0]['x']['type'] );
+               $this->assertEquals( 'http://wikiba.se/ontology#Dump', $result[0]['x']['value'] );
+               $this->assertEquals( 'literal', $result[1]['z']['type'] );
+               $this->assertEquals( '0.1.0', $result[1]['z']['value'] );
+               $this->assertNull( $result[1]['y'] );
+       }
+
+       /**
+        * @expectedException \Mediawiki\Sparql\SparqlException
+        */
+       public function testBadQuery() {
+               $request = $this->getMockBuilder( MWHttpRequest::class )->disableOriginalConstructor()->getMock();
+               $client = new SparqlClient( 'http://acme.test/', $this->getRequestFactory( $request ) );
+
+               $request->method( 'execute' )->willReturn( \Status::newFatal( "Bad query" ) );
+               $result = $client->query( "TEST SPARQL 3" );
+       }
+
+       public function optionsProvider() {
+               return [
+                       'defaults' => [
+                               'TEST тест SPARQL 4 ',
+                               null,
+                               null,
+                               [
+                                       'http://acme.test/',
+                                       'query=TEST+%D1%82%D0%B5%D1%81%D1%82+SPARQL+4+',
+                                       'format=json',
+                                       'maxQueryTimeMillis=30000',
+                               ],
+                               [
+                                       'method' => 'GET',
+                                       'userAgent' => Http::userAgent() . " SparqlClient",
+                                       'timeout' => 30
+                               ]
+                       ],
+                       'big query' => [
+                               str_repeat( 'ZZ', SparqlClient::MAX_GET_SIZE ),
+                               null,
+                               null,
+                               [
+                                       'format=json',
+                                       'maxQueryTimeMillis=30000',
+                               ],
+                               [
+                                       'method' => 'POST',
+                                       'postData' => 'query=' . str_repeat( 'ZZ', SparqlClient::MAX_GET_SIZE ),
+                               ]
+                       ],
+                       'timeout 1s' => [
+                               'TEST SPARQL 4',
+                               null,
+                               1,
+                               [
+                                       'maxQueryTimeMillis=1000',
+                               ],
+                               [
+                                       'timeout' => 1
+                               ]
+                       ],
+                       'more options' => [
+                               'TEST SPARQL 5',
+                               [
+                                       'userAgent' => 'My Test',
+                                       'randomOption' => 'duck',
+                               ],
+                               null,
+                               [],
+                               [
+                                       'userAgent' => 'My Test',
+                                       'randomOption' => 'duck',
+                               ]
+                       ],
+
+               ];
+       }
+
+       /**
+        * @dataProvider  optionsProvider
+        * @param string $sparql
+        * @param array|null $options
+        * @param int|null $timeout
+        * @param array $expectedUrl
+        * @param array $expectedOptions
+        */
+       public function testOptions( $sparql, $options, $timeout, $expectedUrl, $expectedOptions ) {
+               $requestFactory = $this->getMock( HttpRequestFactory::class );
+               $client = new SparqlClient( 'http://acme.test/',  $requestFactory );
+
+               $request = $this->getRequestMock( '{}' );
+
+               $requestFactory->method( 'create' )->willReturnCallback(
+                       function ( $url, $options ) use ( $request, $expectedUrl, $expectedOptions ) {
+                               foreach ( $expectedUrl as $eurl ) {
+                                       $this->assertContains( $eurl, $url );
+                               }
+                               foreach ( $expectedOptions as $ekey => $evalue ) {
+                                       $this->assertArrayHasKey( $ekey, $options );
+                                       $this->assertEquals( $options[$ekey], $evalue );
+                               }
+                               return $request;
+                       }
+               );
+
+               if ( !is_null( $options ) ) {
+                       $client->setClientOptions( $options );
+               }
+               if ( !is_null( $timeout ) ) {
+                       $client->setTimeout( $timeout );
+               }
+
+               $result = $client->query( $sparql );
+       }
+
+}
diff --git a/tests/phpunit/unit/includes/specials/ImageListPagerTest.php b/tests/phpunit/unit/includes/specials/ImageListPagerTest.php
new file mode 100644 (file)
index 0000000..ce0972e
--- /dev/null
@@ -0,0 +1,25 @@
+<?php
+/**
+ * Test class for ImageListPagerTest class.
+ *
+ * Copyright © 2013, Antoine Musso
+ * Copyright © 2013, Siebrand Mazeland
+ * Copyright © 2013, Wikimedia Foundation Inc.
+ *
+ * @group Database
+ */
+class ImageListPagerTest extends \MediaWikiUnitTestCase {
+       /**
+        * @expectedException MWException
+        * @expectedExceptionMessage invalid_field
+        * @covers ImageListPager::formatValue
+        */
+       public function testFormatValuesThrowException() {
+               $page = $this->getMockBuilder( ImageListPager::class )
+                       ->disableOriginalConstructor()
+                       ->setMethods( null )
+                       ->getMock();
+
+               $page->formatValue( 'invalid_field', 'invalid_value' );
+       }
+}
diff --git a/tests/phpunit/unit/includes/specials/SpecialUploadTest.php b/tests/phpunit/unit/includes/specials/SpecialUploadTest.php
new file mode 100644 (file)
index 0000000..a8e3ded
--- /dev/null
@@ -0,0 +1,29 @@
+<?php
+
+class SpecialUploadTest extends \MediaWikiUnitTestCase {
+       /**
+        * @covers SpecialUpload::getInitialPageText
+        * @dataProvider provideGetInitialPageText
+        */
+       public function testGetInitialPageText( $expected, $inputParams ) {
+               $result = call_user_func_array( [ 'SpecialUpload', 'getInitialPageText' ], $inputParams );
+               $this->assertEquals( $expected, $result );
+       }
+
+       public function provideGetInitialPageText() {
+               return [
+                       [
+                               'expect' => "== Summary ==\nthis is a test\n",
+                               'params' => [
+                                       'this is a test'
+                               ],
+                       ],
+                       [
+                               'expect' => "== Summary ==\nthis is a test\n",
+                               'params' => [
+                                       "== Summary ==\nthis is a test",
+                               ],
+                       ],
+               ];
+       }
+}
diff --git a/tests/phpunit/unit/includes/specials/UncategorizedCategoriesPageTest.php b/tests/phpunit/unit/includes/specials/UncategorizedCategoriesPageTest.php
new file mode 100644 (file)
index 0000000..522ca86
--- /dev/null
@@ -0,0 +1,80 @@
+<?php
+/**
+ * Tests for Special:Uncategorizedcategories
+ */
+class UncategorizedCategoriesPageTest extends \MediaWikiUnitTestCase {
+
+       protected function setUp() {
+               parent::setUp();
+
+               $loadBalancerMock = $this->createMock( LoadBalancer::class );
+
+               $loadBalancerMock->expects( $this->any() )
+                       ->method( 'getConnection' )
+                       ->willReturn( new DatabaseTestHelper( __CLASS__ ) );
+
+               $loadBalancerMockFactory = function () use ( $loadBalancerMock ): LoadBalancer {
+                       return $loadBalancerMock;
+               };
+
+               $this->overrideMwServices( [ 'DBLoadBalancer' => $loadBalancerMockFactory ] );
+       }
+
+       /**
+        * @dataProvider provideTestGetQueryInfoData
+        * @covers UncategorizedCategoriesPage::getQueryInfo
+        */
+       public function testGetQueryInfo( $msgContent, $expected ) {
+               $msg = new RawMessage( $msgContent );
+               $mockContext = $this->getMockBuilder( RequestContext::class )->getMock();
+               $mockContext->method( 'msg' )->willReturn( $msg );
+               $special = new UncategorizedCategoriesPage();
+               $special->setContext( $mockContext );
+               $this->assertEquals( [
+                       'tables' => [
+                               0 => 'page',
+                               1 => 'categorylinks',
+                       ],
+                       'fields' => [
+                               'namespace' => 'page_namespace',
+                               'title' => 'page_title',
+                               'value' => 'page_title',
+                       ],
+                       'conds' => [
+                               0 => 'cl_from IS NULL',
+                               'page_namespace' => 14,
+                               'page_is_redirect' => 0,
+                       ] + $expected,
+                       'join_conds' => [
+                               'categorylinks' => [
+                                       0 => 'LEFT JOIN',
+                                       1 => 'cl_from = page_id',
+                               ],
+                       ],
+               ], $special->getQueryInfo() );
+       }
+
+       public function provideTestGetQueryInfoData() {
+               return [
+                       [
+                               "* Stubs\n* Test\n* *\n* * test123",
+                               [ 1 => "page_title not in ( 'Stubs','Test','*','*_test123' )" ]
+                       ],
+                       [
+                               "Stubs\n* Test\n* *\n* * test123",
+                               [ 1 => "page_title not in ( 'Test','*','*_test123' )" ]
+                       ],
+                       [
+                               "* StubsTest\n* *\n* * test123",
+                               [ 1 => "page_title not in ( 'StubsTest','*','*_test123' )" ]
+                       ],
+                       [ "", [] ],
+                       [ "\n\n\n", [] ],
+                       [ "\n", [] ],
+                       [ "Test\n*Test2", [ 1 => "page_title not in ( 'Test2' )" ] ],
+                       [ "Test", [] ],
+                       [ "*Test\nTest2", [ 1 => "page_title not in ( 'Test' )" ] ],
+                       [ "Test\nTest2", [] ],
+               ];
+       }
+}
diff --git a/tests/phpunit/unit/includes/tidy/RemexDriverTest.php b/tests/phpunit/unit/includes/tidy/RemexDriverTest.php
new file mode 100644 (file)
index 0000000..24a5b25
--- /dev/null
@@ -0,0 +1,326 @@
+<?php
+
+class RemexDriverTest extends \MediaWikiUnitTestCase {
+       private static $remexTidyTestData = [
+               [
+                       'Empty string',
+                       "",
+                       ""
+               ],
+               [
+                       'Simple p-wrap',
+                       "x",
+                       "<p>x</p>"
+               ],
+               [
+                       'No p-wrap of blank node',
+                       " ",
+                       " "
+               ],
+               [
+                       'p-wrap terminated by div',
+                       "x<div></div>",
+                       "<p>x</p><div></div>"
+               ],
+               [
+                       'p-wrap not terminated by span',
+                       "x<span></span>",
+                       "<p>x<span></span></p>"
+               ],
+               [
+                       'An element is non-blank and so gets p-wrapped',
+                       "<span></span>",
+                       "<p><span></span></p>"
+               ],
+               [
+                       'The blank flag is set after a block-level element',
+                       "<div></div> ",
+                       "<div></div> "
+               ],
+               [
+                       'Blank detection between two block-level elements',
+                       "<div></div> <div></div>",
+                       "<div></div> <div></div>"
+               ],
+               [
+                       'But p-wrapping of non-blank content works after an element',
+                       "<div></div>x",
+                       "<div></div><p>x</p>"
+               ],
+               [
+                       'p-wrapping between two block-level elements',
+                       "<div></div>x<div></div>",
+                       "<div></div><p>x</p><div></div>"
+               ],
+               [
+                       'p-wrap inside blockquote',
+                       "<blockquote>x</blockquote>",
+                       "<blockquote><p>x</p></blockquote>"
+               ],
+               [
+                       'A comment is blank for p-wrapping purposes',
+                       "<!-- x -->",
+                       "<!-- x -->"
+               ],
+               [
+                       'A comment is blank even when a p-wrap was opened by a text node',
+                       " <!-- x -->",
+                       " <!-- x -->"
+               ],
+               [
+                       'A comment does not open a p-wrap',
+                       "<!-- x -->x",
+                       "<!-- x --><p>x</p>"
+               ],
+               [
+                       'A comment does not close a p-wrap',
+                       "x<!-- x -->",
+                       "<p>x<!-- x --></p>"
+               ],
+               [
+                       'Empty li',
+                       "<ul><li></li></ul>",
+                       "<ul><li class=\"mw-empty-elt\"></li></ul>"
+               ],
+               [
+                       'li with element',
+                       "<ul><li><span></span></li></ul>",
+                       "<ul><li><span></span></li></ul>"
+               ],
+               [
+                       'li with text',
+                       "<ul><li>x</li></ul>",
+                       "<ul><li>x</li></ul>"
+               ],
+               [
+                       'Empty tr',
+                       "<table><tbody><tr></tr></tbody></table>",
+                       "<table><tbody><tr class=\"mw-empty-elt\"></tr></tbody></table>"
+               ],
+               [
+                       'Empty p',
+                       "<p>\n</p>",
+                       "<p class=\"mw-empty-elt\">\n</p>"
+               ],
+               [
+                       'No p-wrapping of an inline element which contains a block element (T150317)',
+                       "<small><div>x</div></small>",
+                       "<small><div>x</div></small>"
+               ],
+               [
+                       'p-wrapping of an inline element which contains an inline element',
+                       "<small><b>x</b></small>",
+                       "<p><small><b>x</b></small></p>"
+               ],
+               [
+                       'p-wrapping is enabled in a blockquote in an inline element',
+                       "<small><blockquote>x</blockquote></small>",
+                       "<small><blockquote><p>x</p></blockquote></small>"
+               ],
+               [
+                       'All bare text should be p-wrapped even when surrounded by block tags',
+                       "<small><blockquote>x</blockquote></small>y<div></div>z",
+                       "<small><blockquote><p>x</p></blockquote></small><p>y</p><div></div><p>z</p>"
+               ],
+               [
+                       'Split tag stack 1',
+                       "<small>x<div>y</div>z</small>",
+                       "<p><small>x</small></p><small><div>y</div></small><p><small>z</small></p>"
+               ],
+               [
+                       'Split tag stack 2',
+                       "<small><div>y</div>z</small>",
+                       "<small><div>y</div></small><p><small>z</small></p>"
+               ],
+               [
+                       'Split tag stack 3',
+                       "<small>x<div>y</div></small>",
+                       "<p><small>x</small></p><small><div>y</div></small>"
+               ],
+               [
+                       'Split tag stack 4 (modified to use splittable tag)',
+                       "a<code>b<i>c<div>d</div></i>e</code>",
+                       "<p>a<code>b<i>c</i></code></p><code><i><div>d</div></i></code><p><code>e</code></p>"
+               ],
+               [
+                       "Split tag stack regression check 1",
+                       "x<span><div>y</div></span>",
+                       "<p>x</p><span><div>y</div></span>"
+               ],
+               [
+                       "Split tag stack regression check 2 (modified to use splittable tag)",
+                       "a<code><i><div>d</div></i>e</code>",
+                       "<p>a</p><code><i><div>d</div></i></code><p><code>e</code></p>"
+               ],
+               // Simple tests from pwrap.js
+               [
+                       'Simple pwrap test 1',
+                       'a',
+                       '<p>a</p>'
+               ],
+               [
+                       '<span> is not a splittable tag, but gets p-wrapped in simple wrapping scenarios',
+                       '<span>a</span>',
+                       '<p><span>a</span></p>'
+               ],
+               [
+                       'Simple pwrap test 3',
+                       'x <div>a</div> <div>b</div> y',
+                       '<p>x </p><div>a</div> <div>b</div><p> y</p>'
+               ],
+               [
+                       'Simple pwrap test 4',
+                       'x<!--c--> <div>a</div> <div>b</div> <!--c-->y',
+                       '<p>x<!--c--> </p><div>a</div> <div>b</div> <!--c--><p>y</p>'
+               ],
+               // Complex tests from pwrap.js
+               [
+                       'Complex pwrap test 1',
+                       '<i>x<div>a</div>y</i>',
+                       '<p><i>x</i></p><i><div>a</div></i><p><i>y</i></p>'
+               ],
+               [
+                       'Complex pwrap test 2',
+                       'a<small>b</small><i>c<div>d</div>e</i>f',
+                       '<p>a<small>b</small><i>c</i></p><i><div>d</div></i><p><i>e</i>f</p>'
+               ],
+               [
+                       'Complex pwrap test 3',
+                       'a<small>b<i>c<div>d</div></i>e</small>',
+                       '<p>a<small>b<i>c</i></small></p><small><i><div>d</div></i></small><p><small>e</small></p>'
+               ],
+               [
+                       'Complex pwrap test 4',
+                       'x<small><div>y</div></small>',
+                       '<p>x</p><small><div>y</div></small>'
+               ],
+               [
+                       'Complex pwrap test 5',
+                       'a<small><i><div>d</div></i>e</small>',
+                       '<p>a</p><small><i><div>d</div></i></small><p><small>e</small></p>'
+               ],
+               // phpcs:disable Generic.Files.LineLength
+               [
+                       'Complex pwrap test 6',
+                       '<i>a<div>b</div>c<b>d<div>e</div>f</b>g</i>',
+                       // PHP 5 does not allow concatenation in initialisation of a class static variable
+                       '<p><i>a</i></p><i><div>b</div></i><p><i>c<b>d</b></i></p><i><b><div>e</div></b></i><p><i><b>f</b>g</i></p>'
+               ],
+               // phpcs:enable
+               /* FIXME the second <b> causes a stack split which clones the <i> even
+                * though no <p> is actually generated
+               [
+                       'Complex pwrap test 7',
+                       '<i><b><font><div>x</div></font></b><div>y</div><b><font><div>z</div></font></b></i>',
+                       '<i><b><font><div>x</div></font></b><div>y</div><b><font><div>z</div></font></b></i>'
+               ],
+                */
+               // New local tests
+               [
+                       'Blank text node after block end',
+                       '<small>x<div>y</div> <b>z</b></small>',
+                       '<p><small>x</small></p><small><div>y</div></small><p><small> <b>z</b></small></p>'
+               ],
+               [
+                       'Text node fostering (FIXME: wrap missing)',
+                       '<table>x</table>',
+                       'x<table></table>'
+               ],
+               [
+                       'Blockquote fostering',
+                       '<table><blockquote>x</blockquote></table>',
+                       '<blockquote><p>x</p></blockquote><table></table>'
+               ],
+               [
+                       'Block element fostering',
+                       '<table><div>x',
+                       '<div>x</div><table></table>'
+               ],
+               [
+                       'Formatting element fostering (FIXME: wrap missing)',
+                       '<table><b>x',
+                       '<b>x</b><table></table>'
+               ],
+               [
+                       'AAA clone of p-wrapped element (FIXME: empty b)',
+                       '<b>x<p>y</b>z</p>',
+                       '<p><b>x</b></p><b></b><p><b>y</b>z</p>',
+               ],
+               [
+                       'AAA with fostering (FIXME: wrap missing)',
+                       '<table><b>1<p>2</b>3</p>',
+                       '<b>1</b><p><b>2</b>3</p><table></table>'
+               ],
+               [
+                       'AAA causes reparent of p-wrapped text node (T178632)',
+                       '<i><blockquote>x</i></blockquote>',
+                       '<i></i><blockquote><p><i>x</i></p></blockquote>',
+               ],
+               [
+                       'p-wrap ended by reparenting (T200827)',
+                       '<i><blockquote><p></i>',
+                       '<i></i><blockquote><p><i></i></p><p><i></i></p></blockquote>',
+               ],
+               [
+                       'style tag isn\'t p-wrapped (T186965)',
+                       '<style>/* ... */</style>',
+                       '<style>/* ... */</style>',
+               ],
+               [
+                       'link tag isn\'t p-wrapped (T186965)',
+                       '<link rel="foo" href="bar" />',
+                       '<link rel="foo" href="bar" />',
+               ],
+               [
+                       'style tag doesn\'t split p-wrapping (T208901)',
+                       'foo <style>/* ... */</style> bar',
+                       '<p>foo <style>/* ... */</style> bar</p>',
+               ],
+               [
+                       'link tag doesn\'t split p-wrapping (T208901)',
+                       'foo <link rel="foo" href="bar" /> bar',
+                       '<p>foo <link rel="foo" href="bar" /> bar</p>',
+               ],
+       ];
+
+       public function provider() {
+               return self::$remexTidyTestData;
+       }
+
+       /**
+        * @dataProvider provider
+        * @covers MediaWiki\Tidy\RemexCompatFormatter
+        * @covers MediaWiki\Tidy\RemexCompatMunger
+        * @covers MediaWiki\Tidy\RemexDriver
+        * @covers MediaWiki\Tidy\RemexMungerData
+        */
+       public function testTidy( $desc, $input, $expected ) {
+               $r = new MediaWiki\Tidy\RemexDriver( [] );
+               $result = $r->tidy( $input );
+               $this->assertEquals( $expected, $result, $desc );
+       }
+
+       public function html5libProvider() {
+               $files = json_decode( file_get_contents( __DIR__ . '/html5lib-tests.json' ), true );
+               $tests = [];
+               foreach ( $files as $file => $fileTests ) {
+                       foreach ( $fileTests as $i => $test ) {
+                               $tests[] = [ "$file:$i", $test['data'] ];
+                       }
+               }
+               return $tests;
+       }
+
+       /**
+        * This is a quick and dirty test to make sure none of the html5lib tests
+        * generate exceptions. We don't really know what the expected output is.
+        *
+        * @dataProvider html5libProvider
+        * @coversNothing
+        */
+       public function testHtml5Lib( $desc, $input ) {
+               $r = new MediaWiki\Tidy\RemexDriver( [] );
+               $result = $r->tidy( $input );
+               $this->assertTrue( true, $desc );
+       }
+}
diff --git a/tests/phpunit/unit/includes/tidy/html5lib-tests.json b/tests/phpunit/unit/includes/tidy/html5lib-tests.json
new file mode 100644 (file)
index 0000000..2b1c3e8
--- /dev/null
@@ -0,0 +1,80692 @@
+{
+  "adoption01.dat": [
+    {
+      "data": "<a><p></a></p>",
+      "errors": [
+        "(1,3): expected-doctype-but-got-start-tag",
+        "(1,10): adoption-agency-1.3"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "a": true,
+            "p": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "a"
+                  },
+                  {
+                    "tag": "p",
+                    "children": [
+                      {
+                        "tag": "a"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><a></a><p><a></a></p></body></html>",
+        "noQuirksBodyHtml": "<a></a><p><a></a></p>"
+      }
+    },
+    {
+      "data": "<a>1<p>2</a>3</p>",
+      "errors": [
+        "(1,3): expected-doctype-but-got-start-tag",
+        "(1,12): adoption-agency-1.3"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "a": true,
+            "p": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "a",
+                    "children": [
+                      {
+                        "text": "1"
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "p",
+                    "children": [
+                      {
+                        "tag": "a",
+                        "children": [
+                          {
+                            "text": "2"
+                          }
+                        ]
+                      },
+                      {
+                        "text": "3"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><a>1</a><p><a>2</a>3</p></body></html>",
+        "noQuirksBodyHtml": "<a>1</a><p><a>2</a>3</p>"
+      }
+    },
+    {
+      "data": "<a>1<button>2</a>3</button>",
+      "errors": [
+        "(1,3): expected-doctype-but-got-start-tag",
+        "(1,17): adoption-agency-1.3"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "a": true,
+            "button": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "a",
+                    "children": [
+                      {
+                        "text": "1"
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "button",
+                    "children": [
+                      {
+                        "tag": "a",
+                        "children": [
+                          {
+                            "text": "2"
+                          }
+                        ]
+                      },
+                      {
+                        "text": "3"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><a>1</a><button><a>2</a>3</button></body></html>",
+        "noQuirksBodyHtml": "<a>1</a><button><a>2</a>3</button>"
+      }
+    },
+    {
+      "data": "<a>1<b>2</a>3</b>",
+      "errors": [
+        "(1,3): expected-doctype-but-got-start-tag",
+        "(1,12): adoption-agency-1.3"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "a": true,
+            "b": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "a",
+                    "children": [
+                      {
+                        "text": "1"
+                      },
+                      {
+                        "tag": "b",
+                        "children": [
+                          {
+                            "text": "2"
+                          }
+                        ]
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "b",
+                    "children": [
+                      {
+                        "text": "3"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><a>1<b>2</b></a><b>3</b></body></html>",
+        "noQuirksBodyHtml": "<a>1<b>2</b></a><b>3</b>"
+      }
+    },
+    {
+      "data": "<a>1<div>2<div>3</a>4</div>5</div>",
+      "errors": [
+        "(1,3): expected-doctype-but-got-start-tag",
+        "(1,20): adoption-agency-1.3",
+        "(1,20): adoption-agency-1.3"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "a": true,
+            "div": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "a",
+                    "children": [
+                      {
+                        "text": "1"
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "div",
+                    "children": [
+                      {
+                        "tag": "a",
+                        "children": [
+                          {
+                            "text": "2"
+                          }
+                        ]
+                      },
+                      {
+                        "tag": "div",
+                        "children": [
+                          {
+                            "tag": "a",
+                            "children": [
+                              {
+                                "text": "3"
+                              }
+                            ]
+                          },
+                          {
+                            "text": "4"
+                          }
+                        ]
+                      },
+                      {
+                        "text": "5"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><a>1</a><div><a>2</a><div><a>3</a>4</div>5</div></body></html>",
+        "noQuirksBodyHtml": "<a>1</a><div><a>2</a><div><a>3</a>4</div>5</div>"
+      }
+    },
+    {
+      "data": "<table><a>1<p>2</a>3</p>",
+      "errors": [
+        "(1,7): expected-doctype-but-got-start-tag",
+        "(1,10): unexpected-start-tag-implies-table-voodoo",
+        "(1,11): unexpected-character-implies-table-voodoo",
+        "(1,14): unexpected-start-tag-implies-table-voodoo",
+        "(1,15): unexpected-character-implies-table-voodoo",
+        "(1,19): unexpected-end-tag-implies-table-voodoo",
+        "(1,19): adoption-agency-1.3",
+        "(1,20): unexpected-character-implies-table-voodoo",
+        "(1,24): unexpected-end-tag-implies-table-voodoo",
+        "(1,24): eof-in-table"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "a": true,
+            "p": true,
+            "table": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "a",
+                    "children": [
+                      {
+                        "text": "1"
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "p",
+                    "children": [
+                      {
+                        "tag": "a",
+                        "children": [
+                          {
+                            "text": "2"
+                          }
+                        ]
+                      },
+                      {
+                        "text": "3"
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "table"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><a>1</a><p><a>2</a>3</p><table></table></body></html>",
+        "noQuirksBodyHtml": "<a>1</a><p><a>2</a>3</p><table></table>"
+      }
+    },
+    {
+      "data": "<b><b><a><p></a>",
+      "errors": [
+        "(1,3): expected-doctype-but-got-start-tag",
+        "(1,16): adoption-agency-1.3",
+        "(1,16): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "b": true,
+            "a": true,
+            "p": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "b",
+                    "children": [
+                      {
+                        "tag": "b",
+                        "children": [
+                          {
+                            "tag": "a"
+                          },
+                          {
+                            "tag": "p",
+                            "children": [
+                              {
+                                "tag": "a"
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><b><b><a></a><p><a></a></p></b></b></body></html>",
+        "noQuirksBodyHtml": "<b><b><a></a><p><a></a></p></b></b>"
+      }
+    },
+    {
+      "data": "<b><a><b><p></a>",
+      "errors": [
+        "(1,3): expected-doctype-but-got-start-tag",
+        "(1,16): adoption-agency-1.3",
+        "(1,16): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "b": true,
+            "a": true,
+            "p": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "b",
+                    "children": [
+                      {
+                        "tag": "a",
+                        "children": [
+                          {
+                            "tag": "b"
+                          }
+                        ]
+                      },
+                      {
+                        "tag": "b",
+                        "children": [
+                          {
+                            "tag": "p",
+                            "children": [
+                              {
+                                "tag": "a"
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><b><a><b></b></a><b><p><a></a></p></b></b></body></html>",
+        "noQuirksBodyHtml": "<b><a><b></b></a><b><p><a></a></p></b></b>"
+      }
+    },
+    {
+      "data": "<a><b><b><p></a>",
+      "errors": [
+        "(1,3): expected-doctype-but-got-start-tag",
+        "(1,16): adoption-agency-1.3",
+        "(1,16): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "a": true,
+            "b": true,
+            "p": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "a",
+                    "children": [
+                      {
+                        "tag": "b",
+                        "children": [
+                          {
+                            "tag": "b"
+                          }
+                        ]
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "b",
+                    "children": [
+                      {
+                        "tag": "b",
+                        "children": [
+                          {
+                            "tag": "p",
+                            "children": [
+                              {
+                                "tag": "a"
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><a><b><b></b></b></a><b><b><p><a></a></p></b></b></body></html>",
+        "noQuirksBodyHtml": "<a><b><b></b></b></a><b><b><p><a></a></p></b></b>"
+      }
+    },
+    {
+      "data": "<p>1<s id=\"A\">2<b id=\"B\">3</p>4</s>5</b>",
+      "errors": [
+        "(1,3): expected-doctype-but-got-start-tag",
+        "(1,30): unexpected-end-tag",
+        "(1,35): adoption-agency-1.3"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "p": true,
+            "s": true,
+            "b": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "p",
+                    "children": [
+                      {
+                        "text": "1"
+                      },
+                      {
+                        "tag": "s",
+                        "attrs": [
+                          {
+                            "name": "id",
+                            "value": "A"
+                          }
+                        ],
+                        "children": [
+                          {
+                            "text": "2"
+                          },
+                          {
+                            "tag": "b",
+                            "attrs": [
+                              {
+                                "name": "id",
+                                "value": "B"
+                              }
+                            ],
+                            "children": [
+                              {
+                                "text": "3"
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "s",
+                    "attrs": [
+                      {
+                        "name": "id",
+                        "value": "A"
+                      }
+                    ],
+                    "children": [
+                      {
+                        "tag": "b",
+                        "attrs": [
+                          {
+                            "name": "id",
+                            "value": "B"
+                          }
+                        ],
+                        "children": [
+                          {
+                            "text": "4"
+                          }
+                        ]
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "b",
+                    "attrs": [
+                      {
+                        "name": "id",
+                        "value": "B"
+                      }
+                    ],
+                    "children": [
+                      {
+                        "text": "5"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><p>1<s id=\"A\">2<b id=\"B\">3</b></s></p><s id=\"A\"><b id=\"B\">4</b></s><b id=\"B\">5</b></body></html>",
+        "noQuirksBodyHtml": "<p>1<s id=\"A\">2<b id=\"B\">3</b></s></p><s id=\"A\"><b id=\"B\">4</b></s><b id=\"B\">5</b>"
+      }
+    },
+    {
+      "data": "<table><a>1<td>2</td>3</table>",
+      "errors": [
+        "(1,7): expected-doctype-but-got-start-tag",
+        "(1,10): unexpected-start-tag-implies-table-voodoo",
+        "(1,11): unexpected-character-implies-table-voodoo",
+        "(1,15): unexpected-cell-in-table-body",
+        "(1,30): unexpected-implied-end-tag-in-table-view"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "a": true,
+            "table": true,
+            "tbody": true,
+            "tr": true,
+            "td": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "a",
+                    "children": [
+                      {
+                        "text": "1"
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "a",
+                    "children": [
+                      {
+                        "text": "3"
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "tbody",
+                        "children": [
+                          {
+                            "tag": "tr",
+                            "children": [
+                              {
+                                "tag": "td",
+                                "children": [
+                                  {
+                                    "text": "2"
+                                  }
+                                ]
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><a>1</a><a>3</a><table><tbody><tr><td>2</td></tr></tbody></table></body></html>",
+        "noQuirksBodyHtml": "<a>1</a><a>3</a><table><tbody><tr><td>2</td></tr></tbody></table>"
+      }
+    },
+    {
+      "data": "<table>A<td>B</td>C</table>",
+      "errors": [
+        "(1,7): expected-doctype-but-got-start-tag",
+        "(1,8): unexpected-character-implies-table-voodoo",
+        "(1,12): unexpected-cell-in-table-body",
+        "(1,22): unexpected-character-implies-table-voodoo"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true,
+            "tbody": true,
+            "tr": true,
+            "td": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "AC"
+                  },
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "tbody",
+                        "children": [
+                          {
+                            "tag": "tr",
+                            "children": [
+                              {
+                                "tag": "td",
+                                "children": [
+                                  {
+                                    "text": "B"
+                                  }
+                                ]
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>AC<table><tbody><tr><td>B</td></tr></tbody></table></body></html>",
+        "noQuirksBodyHtml": "AC<table><tbody><tr><td>B</td></tr></tbody></table>"
+      }
+    },
+    {
+      "data": "<a><svg><tr><input></a>",
+      "errors": [
+        "(1,3): expected-doctype-but-got-start-tag",
+        "(1,23): unexpected-end-tag",
+        "(1,23): adoption-agency-1.3"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "a": true,
+            "svg svg": true,
+            "svg tr": true,
+            "svg input": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "a",
+                    "children": [
+                      {
+                        "tag": "svg",
+                        "ns": "http://www.w3.org/2000/svg",
+                        "children": [
+                          {
+                            "tag": "tr",
+                            "ns": "http://www.w3.org/2000/svg",
+                            "children": [
+                              {
+                                "tag": "input",
+                                "ns": "http://www.w3.org/2000/svg"
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><a><svg><tr><input></input></tr></svg></a></body></html>",
+        "noQuirksBodyHtml": "<a><svg><tr><input></input></tr></svg></a>"
+      }
+    },
+    {
+      "data": "<div><a><b><div><div><div><div><div><div><div><div><div><div></a>",
+      "errors": [
+        "(1,5): expected-doctype-but-got-start-tag",
+        "(1,65): adoption-agency-1.3",
+        "(1,65): adoption-agency-1.3",
+        "(1,65): adoption-agency-1.3",
+        "(1,65): adoption-agency-1.3",
+        "(1,65): adoption-agency-1.3",
+        "(1,65): adoption-agency-1.3",
+        "(1,65): adoption-agency-1.3",
+        "(1,65): adoption-agency-1.3",
+        "(1,65): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "div": true,
+            "a": true,
+            "b": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "div",
+                    "children": [
+                      {
+                        "tag": "a",
+                        "children": [
+                          {
+                            "tag": "b"
+                          }
+                        ]
+                      },
+                      {
+                        "tag": "b",
+                        "children": [
+                          {
+                            "tag": "div",
+                            "children": [
+                              {
+                                "tag": "a"
+                              },
+                              {
+                                "tag": "div",
+                                "children": [
+                                  {
+                                    "tag": "a"
+                                  },
+                                  {
+                                    "tag": "div",
+                                    "children": [
+                                      {
+                                        "tag": "a"
+                                      },
+                                      {
+                                        "tag": "div",
+                                        "children": [
+                                          {
+                                            "tag": "a"
+                                          },
+                                          {
+                                            "tag": "div",
+                                            "children": [
+                                              {
+                                                "tag": "a"
+                                              },
+                                              {
+                                                "tag": "div",
+                                                "children": [
+                                                  {
+                                                    "tag": "a"
+                                                  },
+                                                  {
+                                                    "tag": "div",
+                                                    "children": [
+                                                      {
+                                                        "tag": "a"
+                                                      },
+                                                      {
+                                                        "tag": "div",
+                                                        "children": [
+                                                          {
+                                                            "tag": "a",
+                                                            "children": [
+                                                              {
+                                                                "tag": "div",
+                                                                "children": [
+                                                                  {
+                                                                    "tag": "div"
+                                                                  }
+                                                                ]
+                                                              }
+                                                            ]
+                                                          }
+                                                        ]
+                                                      }
+                                                    ]
+                                                  }
+                                                ]
+                                              }
+                                            ]
+                                          }
+                                        ]
+                                      }
+                                    ]
+                                  }
+                                ]
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><div><a><b></b></a><b><div><a></a><div><a></a><div><a></a><div><a></a><div><a></a><div><a></a><div><a></a><div><a><div><div></div></div></a></div></div></div></div></div></div></div></div></b></div></body></html>",
+        "noQuirksBodyHtml": "<div><a><b></b></a><b><div><a></a><div><a></a><div><a></a><div><a></a><div><a></a><div><a></a><div><a></a><div><a><div><div></div></div></a></div></div></div></div></div></div></div></div></b></div>"
+      }
+    },
+    {
+      "data": "<div><a><b><u><i><code><div></a>",
+      "errors": [
+        "(1,5): expected-doctype-but-got-start-tag",
+        "(1,32): adoption-agency-1.3",
+        "(1,32): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "div": true,
+            "a": true,
+            "b": true,
+            "u": true,
+            "i": true,
+            "code": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "div",
+                    "children": [
+                      {
+                        "tag": "a",
+                        "children": [
+                          {
+                            "tag": "b",
+                            "children": [
+                              {
+                                "tag": "u",
+                                "children": [
+                                  {
+                                    "tag": "i",
+                                    "children": [
+                                      {
+                                        "tag": "code"
+                                      }
+                                    ]
+                                  }
+                                ]
+                              }
+                            ]
+                          }
+                        ]
+                      },
+                      {
+                        "tag": "u",
+                        "children": [
+                          {
+                            "tag": "i",
+                            "children": [
+                              {
+                                "tag": "code",
+                                "children": [
+                                  {
+                                    "tag": "div",
+                                    "children": [
+                                      {
+                                        "tag": "a"
+                                      }
+                                    ]
+                                  }
+                                ]
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><div><a><b><u><i><code></code></i></u></b></a><u><i><code><div><a></a></div></code></i></u></div></body></html>",
+        "noQuirksBodyHtml": "<div><a><b><u><i><code></code></i></u></b></a><u><i><code><div><a></a></div></code></i></u></div>"
+      }
+    },
+    {
+      "data": "<b><b><b><b>x</b></b></b></b>y",
+      "errors": [
+        "(1,3): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "b": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "b",
+                    "children": [
+                      {
+                        "tag": "b",
+                        "children": [
+                          {
+                            "tag": "b",
+                            "children": [
+                              {
+                                "tag": "b",
+                                "children": [
+                                  {
+                                    "text": "x"
+                                  }
+                                ]
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  },
+                  {
+                    "text": "y"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><b><b><b><b>x</b></b></b></b>y</body></html>",
+        "noQuirksBodyHtml": "<b><b><b><b>x</b></b></b></b>y"
+      }
+    },
+    {
+      "data": "<p><b><b><b><b><p>x",
+      "errors": [
+        "(1,3): expected-doctype-but-got-start-tag",
+        "(1,18): unexpected-end-tag",
+        "(1,19): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "p": true,
+            "b": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "p",
+                    "children": [
+                      {
+                        "tag": "b",
+                        "children": [
+                          {
+                            "tag": "b",
+                            "children": [
+                              {
+                                "tag": "b",
+                                "children": [
+                                  {
+                                    "tag": "b"
+                                  }
+                                ]
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "p",
+                    "children": [
+                      {
+                        "tag": "b",
+                        "children": [
+                          {
+                            "tag": "b",
+                            "children": [
+                              {
+                                "tag": "b",
+                                "children": [
+                                  {
+                                    "text": "x"
+                                  }
+                                ]
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><p><b><b><b><b></b></b></b></b></p><p><b><b><b>x</b></b></b></p></body></html>",
+        "noQuirksBodyHtml": "<p><b><b><b><b></b></b></b></b></p><p><b><b><b>x</b></b></b></p>"
+      }
+    },
+    {
+      "data": "<b><em><foo><foob><fooc><aside></b></em>",
+      "errors": [
+        "(1,35): adoption-agency-1.3",
+        "(1,40): adoption-agency-1.3",
+        "(1,40): expected-closing-tag-but-got-eof"
+      ],
+      "fragment": {
+        "name": "div"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "b": true,
+            "em": true,
+            "foo": true,
+            "foob": true,
+            "fooc": true,
+            "aside": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "b",
+            "children": [
+              {
+                "tag": "em",
+                "children": [
+                  {
+                    "tag": "foo",
+                    "children": [
+                      {
+                        "tag": "foob",
+                        "children": [
+                          {
+                            "tag": "fooc"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          },
+          {
+            "tag": "aside",
+            "children": [
+              {
+                "tag": "b"
+              }
+            ]
+          }
+        ],
+        "html": "<b><em><foo><foob><fooc></fooc></foob></foo></em></b><aside><b></b></aside>",
+        "noQuirksBodyHtml": "<b><em><foo><foob><fooc></fooc></foob></foo></em></b><aside><b></b></aside>"
+      }
+    }
+  ],
+  "adoption02.dat": [
+    {
+      "data": "<b>1<i>2<p>3</b>4",
+      "errors": [
+        "(1,3): expected-doctype-but-got-start-tag",
+        "(1,16): adoption-agency-1.3",
+        "(1,17): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "b": true,
+            "i": true,
+            "p": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "b",
+                    "children": [
+                      {
+                        "text": "1"
+                      },
+                      {
+                        "tag": "i",
+                        "children": [
+                          {
+                            "text": "2"
+                          }
+                        ]
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "i",
+                    "children": [
+                      {
+                        "tag": "p",
+                        "children": [
+                          {
+                            "tag": "b",
+                            "children": [
+                              {
+                                "text": "3"
+                              }
+                            ]
+                          },
+                          {
+                            "text": "4"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><b>1<i>2</i></b><i><p><b>3</b>4</p></i></body></html>",
+        "noQuirksBodyHtml": "<b>1<i>2</i></b><i><p><b>3</b>4</p></i>"
+      }
+    },
+    {
+      "data": "<a><div><style></style><address><a>",
+      "errors": [
+        "(1,3): expected-doctype-but-got-start-tag",
+        "(1,35): unexpected-start-tag-implies-end-tag",
+        "(1,35): adoption-agency-1.3",
+        "(1,35): adoption-agency-1.3",
+        "(1,35): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "a": true,
+            "div": true,
+            "style": true,
+            "address": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "a"
+                  },
+                  {
+                    "tag": "div",
+                    "children": [
+                      {
+                        "tag": "a",
+                        "children": [
+                          {
+                            "tag": "style"
+                          }
+                        ]
+                      },
+                      {
+                        "tag": "address",
+                        "children": [
+                          {
+                            "tag": "a"
+                          },
+                          {
+                            "tag": "a"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><a></a><div><a><style></style></a><address><a></a><a></a></address></div></body></html>",
+        "noQuirksBodyHtml": "<a></a><div><a><style></style></a><address><a></a><a></a></address></div>"
+      }
+    }
+  ],
+  "comments01.dat": [
+    {
+      "data": "FOO<!-- BAR -->BAZ",
+      "errors": [
+        "(1,3): expected-doctype-but-got-chars"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "comment": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "FOO"
+                  },
+                  {
+                    "comment": " BAR "
+                  },
+                  {
+                    "text": "BAZ"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>FOO<!-- BAR -->BAZ</body></html>",
+        "noQuirksBodyHtml": "FOO<!-- BAR -->BAZ"
+      }
+    },
+    {
+      "data": "FOO<!-- BAR --!>BAZ",
+      "errors": [
+        "(1,3): expected-doctype-but-got-chars",
+        "(1,15): unexpected-bang-after-double-dash-in-comment"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "comment": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "FOO"
+                  },
+                  {
+                    "comment": " BAR "
+                  },
+                  {
+                    "text": "BAZ"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>FOO<!-- BAR -->BAZ</body></html>",
+        "noQuirksBodyHtml": "FOO<!-- BAR -->BAZ"
+      }
+    },
+    {
+      "data": "FOO<!-- BAR --   >BAZ",
+      "errors": [
+        "(1,3): expected-doctype-but-got-chars",
+        "(1,15): unexpected-char-in-comment",
+        "(1,21): eof-in-comment"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "comment": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "FOO"
+                  },
+                  {
+                    "comment": " BAR --   >BAZ"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>FOO<!-- BAR --   >BAZ--></body></html>",
+        "noQuirksBodyHtml": "FOO<!-- BAR --   >BAZ-->"
+      }
+    },
+    {
+      "data": "FOO<!-- BAR -- <QUX> -- MUX -->BAZ",
+      "errors": [
+        "(1,3): expected-doctype-but-got-chars",
+        "(1,15): unexpected-char-in-comment",
+        "(1,24): unexpected-char-in-comment"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "comment": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "FOO"
+                  },
+                  {
+                    "comment": " BAR -- <QUX> -- MUX "
+                  },
+                  {
+                    "text": "BAZ"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>FOO<!-- BAR -- <QUX> -- MUX -->BAZ</body></html>",
+        "noQuirksBodyHtml": "FOO<!-- BAR -- <QUX> -- MUX -->BAZ"
+      }
+    },
+    {
+      "data": "FOO<!-- BAR -- <QUX> -- MUX --!>BAZ",
+      "errors": [
+        "(1,3): expected-doctype-but-got-chars",
+        "(1,15): unexpected-char-in-comment",
+        "(1,24): unexpected-char-in-comment",
+        "(1,31): unexpected-bang-after-double-dash-in-comment"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "comment": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "FOO"
+                  },
+                  {
+                    "comment": " BAR -- <QUX> -- MUX "
+                  },
+                  {
+                    "text": "BAZ"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>FOO<!-- BAR -- <QUX> -- MUX -->BAZ</body></html>",
+        "noQuirksBodyHtml": "FOO<!-- BAR -- <QUX> -- MUX -->BAZ"
+      }
+    },
+    {
+      "data": "FOO<!-- BAR -- <QUX> -- MUX -- >BAZ",
+      "errors": [
+        "(1,3): expected-doctype-but-got-chars",
+        "(1,15): unexpected-char-in-comment",
+        "(1,24): unexpected-char-in-comment",
+        "(1,31): unexpected-char-in-comment",
+        "(1,35): eof-in-comment"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "comment": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "FOO"
+                  },
+                  {
+                    "comment": " BAR -- <QUX> -- MUX -- >BAZ"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>FOO<!-- BAR -- <QUX> -- MUX -- >BAZ--></body></html>",
+        "noQuirksBodyHtml": "FOO<!-- BAR -- <QUX> -- MUX -- >BAZ-->"
+      }
+    },
+    {
+      "data": "FOO<!---->BAZ",
+      "errors": [
+        "(1,3): expected-doctype-but-got-chars"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "comment": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "FOO"
+                  },
+                  {
+                    "comment": ""
+                  },
+                  {
+                    "text": "BAZ"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>FOO<!---->BAZ</body></html>",
+        "noQuirksBodyHtml": "FOO<!---->BAZ"
+      }
+    },
+    {
+      "data": "FOO<!--->BAZ",
+      "errors": [
+        "(1,3): expected-doctype-but-got-chars",
+        "(1,9): incorrect-comment"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "comment": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "FOO"
+                  },
+                  {
+                    "comment": ""
+                  },
+                  {
+                    "text": "BAZ"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>FOO<!---->BAZ</body></html>",
+        "noQuirksBodyHtml": "FOO<!---->BAZ"
+      }
+    },
+    {
+      "data": "FOO<!-->BAZ",
+      "errors": [
+        "(1,3): expected-doctype-but-got-chars",
+        "(1,8): incorrect-comment"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "comment": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "FOO"
+                  },
+                  {
+                    "comment": ""
+                  },
+                  {
+                    "text": "BAZ"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>FOO<!---->BAZ</body></html>",
+        "noQuirksBodyHtml": "FOO<!---->BAZ"
+      }
+    },
+    {
+      "data": "<?xml version=\"1.0\">Hi",
+      "errors": [
+        "(1,1): expected-tag-name-but-got-question-mark",
+        "(1,22): expected-doctype-but-got-chars"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "comment": true
+        },
+        "tree": [
+          {
+            "comment": "?xml version=\"1.0\""
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "Hi"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!--?xml version=\"1.0\"--><html><head></head><body>Hi</body></html>",
+        "noQuirksBodyHtml": "<!--?xml version=\"1.0\"-->Hi"
+      }
+    },
+    {
+      "data": "<?xml version=\"1.0\">",
+      "errors": [
+        "(1,1): expected-tag-name-but-got-question-mark",
+        "(1,20): expected-doctype-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "comment": true
+        },
+        "tree": [
+          {
+            "comment": "?xml version=\"1.0\""
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!--?xml version=\"1.0\"--><html><head></head><body></body></html>",
+        "noQuirksBodyHtml": "<!--?xml version=\"1.0\"-->"
+      }
+    },
+    {
+      "data": "<?xml version",
+      "errors": [
+        "(1,1): expected-tag-name-but-got-question-mark",
+        "(1,13): expected-doctype-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "comment": true
+        },
+        "tree": [
+          {
+            "comment": "?xml version"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!--?xml version--><html><head></head><body></body></html>",
+        "noQuirksBodyHtml": "<!--?xml version-->"
+      }
+    },
+    {
+      "data": "FOO<!----->BAZ",
+      "errors": [
+        "(1,3): expected-doctype-but-got-chars",
+        "(1,10): unexpected-dash-after-double-dash-in-comment"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "comment": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "FOO"
+                  },
+                  {
+                    "comment": "-"
+                  },
+                  {
+                    "text": "BAZ"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>FOO<!----->BAZ</body></html>",
+        "noQuirksBodyHtml": "FOO<!----->BAZ"
+      }
+    },
+    {
+      "data": "<html><!-- comment --><title>Comment before head</title>",
+      "errors": [
+        "(1,6): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "title": true,
+            "body": true
+          },
+          "comment": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "comment": " comment "
+              },
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "title",
+                    "children": [
+                      {
+                        "text": "Comment before head"
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><!-- comment --><head><title>Comment before head</title></head><body></body></html>",
+        "noQuirksBodyHtml": "<!-- comment --><title>Comment before head</title>"
+      }
+    }
+  ],
+  "doctype01.dat": [
+    {
+      "data": "<!DOCTYPE html>Hello",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "Hello"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body>Hello</body></html>",
+        "noQuirksBodyHtml": "Hello"
+      }
+    },
+    {
+      "data": "<!dOctYpE HtMl>Hello",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "Hello"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body>Hello</body></html>",
+        "noQuirksBodyHtml": "Hello"
+      }
+    },
+    {
+      "data": "<!DOCTYPEhtml>Hello",
+      "errors": [
+        "(1,9): need-space-after-doctype"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "Hello"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body>Hello</body></html>",
+        "noQuirksBodyHtml": "Hello"
+      }
+    },
+    {
+      "data": "<!DOCTYPE>Hello",
+      "errors": [
+        "(1,9): need-space-after-doctype",
+        "(1,10): expected-doctype-name-but-got-right-bracket",
+        "(1,10): unknown-doctype"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": ""
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "Hello"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE ><html><head></head><body>Hello</body></html>",
+        "noQuirksBodyHtml": "Hello"
+      }
+    },
+    {
+      "data": "<!DOCTYPE >Hello",
+      "errors": [
+        "(1,11): expected-doctype-name-but-got-right-bracket",
+        "(1,11): unknown-doctype"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": ""
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "Hello"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE ><html><head></head><body>Hello</body></html>",
+        "noQuirksBodyHtml": "Hello"
+      }
+    },
+    {
+      "data": "<!DOCTYPE potato>Hello",
+      "errors": [
+        "(1,17): unknown-doctype"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "potato"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "Hello"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE potato><html><head></head><body>Hello</body></html>",
+        "noQuirksBodyHtml": "Hello"
+      }
+    },
+    {
+      "data": "<!DOCTYPE potato >Hello",
+      "errors": [
+        "(1,18): unknown-doctype"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "potato"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "Hello"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE potato><html><head></head><body>Hello</body></html>",
+        "noQuirksBodyHtml": "Hello"
+      }
+    },
+    {
+      "data": "<!DOCTYPE potato taco>Hello",
+      "errors": [
+        "(1,17): expected-space-or-right-bracket-in-doctype",
+        "(1,22): unknown-doctype"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "potato"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "Hello"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE potato><html><head></head><body>Hello</body></html>",
+        "noQuirksBodyHtml": "Hello"
+      }
+    },
+    {
+      "data": "<!DOCTYPE potato taco \"ddd>Hello",
+      "errors": [
+        "(1,17): expected-space-or-right-bracket-in-doctype",
+        "(1,27): unknown-doctype"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "potato"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "Hello"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE potato><html><head></head><body>Hello</body></html>",
+        "noQuirksBodyHtml": "Hello"
+      }
+    },
+    {
+      "data": "<!DOCTYPE potato sYstEM>Hello",
+      "errors": [
+        "(1,24): unexpected-char-in-doctype",
+        "(1,24): unknown-doctype"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "potato"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "Hello"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE potato><html><head></head><body>Hello</body></html>",
+        "noQuirksBodyHtml": "Hello"
+      }
+    },
+    {
+      "data": "<!DOCTYPE potato sYstEM    >Hello",
+      "errors": [
+        "(1,28): unexpected-char-in-doctype",
+        "(1,28): unknown-doctype"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "potato"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "Hello"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE potato><html><head></head><body>Hello</body></html>",
+        "noQuirksBodyHtml": "Hello"
+      }
+    },
+    {
+      "data": "<!DOCTYPE   potato       sYstEM  ggg>Hello",
+      "errors": [
+        "(1,34): unexpected-char-in-doctype",
+        "(1,37): unknown-doctype"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "potato"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "Hello"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE potato><html><head></head><body>Hello</body></html>",
+        "noQuirksBodyHtml": "Hello"
+      }
+    },
+    {
+      "data": "<!DOCTYPE potato SYSTEM taco  >Hello",
+      "errors": [
+        "(1,25): unexpected-char-in-doctype",
+        "(1,31): unknown-doctype"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "potato"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "Hello"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE potato><html><head></head><body>Hello</body></html>",
+        "noQuirksBodyHtml": "Hello"
+      }
+    },
+    {
+      "data": "<!DOCTYPE potato SYSTEM 'taco\"'>Hello",
+      "errors": [
+        "(1,32): unknown-doctype"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "potato \"\" \"taco\"\""
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "Hello"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE potato><html><head></head><body>Hello</body></html>",
+        "noQuirksBodyHtml": "Hello"
+      }
+    },
+    {
+      "data": "<!DOCTYPE potato SYSTEM \"taco\">Hello",
+      "errors": [
+        "(1,31): unknown-doctype"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "potato \"\" \"taco\""
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "Hello"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE potato><html><head></head><body>Hello</body></html>",
+        "noQuirksBodyHtml": "Hello"
+      }
+    },
+    {
+      "data": "<!DOCTYPE potato SYSTEM \"tai'co\">Hello",
+      "errors": [
+        "(1,33): unknown-doctype"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "potato \"\" \"tai'co\""
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "Hello"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE potato><html><head></head><body>Hello</body></html>",
+        "noQuirksBodyHtml": "Hello"
+      }
+    },
+    {
+      "data": "<!DOCTYPE potato SYSTEMtaco \"ddd\">Hello",
+      "errors": [
+        "(1,24): unexpected-char-in-doctype",
+        "(1,34): unknown-doctype"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "potato"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "Hello"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE potato><html><head></head><body>Hello</body></html>",
+        "noQuirksBodyHtml": "Hello"
+      }
+    },
+    {
+      "data": "<!DOCTYPE potato grass SYSTEM taco>Hello",
+      "errors": [
+        "(1,17): expected-space-or-right-bracket-in-doctype",
+        "(1,35): unknown-doctype"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "potato"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "Hello"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE potato><html><head></head><body>Hello</body></html>",
+        "noQuirksBodyHtml": "Hello"
+      }
+    },
+    {
+      "data": "<!DOCTYPE potato pUbLIc>Hello",
+      "errors": [
+        "(1,24): unexpected-end-of-doctype",
+        "(1,24): unknown-doctype"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "potato"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "Hello"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE potato><html><head></head><body>Hello</body></html>",
+        "noQuirksBodyHtml": "Hello"
+      }
+    },
+    {
+      "data": "<!DOCTYPE potato pUbLIc >Hello",
+      "errors": [
+        "(1,25): unexpected-end-of-doctype",
+        "(1,25): unknown-doctype"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "potato"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "Hello"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE potato><html><head></head><body>Hello</body></html>",
+        "noQuirksBodyHtml": "Hello"
+      }
+    },
+    {
+      "data": "<!DOCTYPE potato pUbLIcgoof>Hello",
+      "errors": [
+        "(1,24): unexpected-char-in-doctype",
+        "(1,28): unknown-doctype"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "potato"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "Hello"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE potato><html><head></head><body>Hello</body></html>",
+        "noQuirksBodyHtml": "Hello"
+      }
+    },
+    {
+      "data": "<!DOCTYPE potato PUBLIC goof>Hello",
+      "errors": [
+        "(1,25): unexpected-char-in-doctype",
+        "(1,29): unknown-doctype"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "potato"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "Hello"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE potato><html><head></head><body>Hello</body></html>",
+        "noQuirksBodyHtml": "Hello"
+      }
+    },
+    {
+      "data": "<!DOCTYPE potato PUBLIC \"go'of\">Hello",
+      "errors": [
+        "(1,32): unknown-doctype"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "potato \"go'of\" \"\""
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "Hello"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE potato><html><head></head><body>Hello</body></html>",
+        "noQuirksBodyHtml": "Hello"
+      }
+    },
+    {
+      "data": "<!DOCTYPE potato PUBLIC 'go'of'>Hello",
+      "errors": [
+        "(1,29): unexpected-char-in-doctype",
+        "(1,32): unknown-doctype"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "potato \"go\" \"\""
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "Hello"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE potato><html><head></head><body>Hello</body></html>",
+        "noQuirksBodyHtml": "Hello"
+      }
+    },
+    {
+      "data": "<!DOCTYPE potato PUBLIC 'go:hh   of' >Hello",
+      "errors": [
+        "(1,38): unknown-doctype"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "potato \"go:hh   of\" \"\""
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "Hello"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE potato><html><head></head><body>Hello</body></html>",
+        "noQuirksBodyHtml": "Hello"
+      }
+    },
+    {
+      "data": "<!DOCTYPE potato PUBLIC \"W3C-//dfdf\" SYSTEM ggg>Hello",
+      "errors": [
+        "(1,38): unexpected-char-in-doctype",
+        "(1,48): unknown-doctype"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "potato \"W3C-//dfdf\" \"\""
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "Hello"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE potato><html><head></head><body>Hello</body></html>",
+        "noQuirksBodyHtml": "Hello"
+      }
+    },
+    {
+      "data": "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\"\n   \"http://www.w3.org/TR/html4/strict.dtd\">Hello",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd\""
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "Hello"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body>Hello</body></html>",
+        "noQuirksBodyHtml": "Hello"
+      }
+    },
+    {
+      "data": "<!DOCTYPE ...>Hello",
+      "errors": [
+        "(1,14): unknown-doctype"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "..."
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "Hello"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE ...><html><head></head><body>Hello</body></html>",
+        "noQuirksBodyHtml": "Hello"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\"\n\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">",
+      "errors": [
+        "(2,58): unknown-doctype"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\""
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body></body></html>",
+        "noQuirksBodyHtml": ""
+      }
+    },
+    {
+      "data": "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Frameset//EN\"\n\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd\">",
+      "errors": [
+        "(2,54): unknown-doctype"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html \"-//W3C//DTD XHTML 1.0 Frameset//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd\""
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body></body></html>",
+        "noQuirksBodyHtml": ""
+      }
+    },
+    {
+      "data": "<!DOCTYPE root-element [SYSTEM OR PUBLIC FPI] \"uri\" [ \n<!-- internal declarations -->\n]>",
+      "errors": [
+        "(1,23): expected-space-or-right-bracket-in-doctype",
+        "(2,30): unknown-doctype"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "doctype": true,
+          "escaped": true
+        },
+        "tree": [
+          {
+            "doctype": "root-element"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "]>",
+                    "escaped": true
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE root-element><html><head></head><body>]&gt;</body></html>",
+        "noQuirksBodyHtml": "\n]&gt;"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html PUBLIC\n  \"-//WAPFORUM//DTD XHTML Mobile 1.0//EN\"\n    \"http://www.wapforum.org/DTD/xhtml-mobile10.dtd\">",
+      "errors": [
+        "(3,53): unknown-doctype"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html \"-//WAPFORUM//DTD XHTML Mobile 1.0//EN\" \"http://www.wapforum.org/DTD/xhtml-mobile10.dtd\""
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body></body></html>",
+        "noQuirksBodyHtml": ""
+      }
+    },
+    {
+      "data": "<!DOCTYPE HTML SYSTEM \"http://www.w3.org/DTD/HTML4-strict.dtd\"><body><b>Mine!</b></body>",
+      "errors": [
+        "(1,63): unknown-doctype"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "b": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html \"\" \"http://www.w3.org/DTD/HTML4-strict.dtd\""
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "b",
+                    "children": [
+                      {
+                        "text": "Mine!"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><b>Mine!</b></body></html>",
+        "noQuirksBodyHtml": "<b>Mine!</b>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\"\"http://www.w3.org/TR/html4/strict.dtd\">",
+      "errors": [
+        "(1,50): unexpected-char-in-doctype"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd\""
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body></body></html>",
+        "noQuirksBodyHtml": ""
+      }
+    },
+    {
+      "data": "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\"'http://www.w3.org/TR/html4/strict.dtd'>",
+      "errors": [
+        "(1,50): unexpected-char-in-doctype"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd\""
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body></body></html>",
+        "noQuirksBodyHtml": ""
+      }
+    },
+    {
+      "data": "<!DOCTYPE HTML PUBLIC\"-//W3C//DTD HTML 4.01//EN\"'http://www.w3.org/TR/html4/strict.dtd'>",
+      "errors": [
+        "(1,21): unexpected-char-in-doctype",
+        "(1,49): unexpected-char-in-doctype"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd\""
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body></body></html>",
+        "noQuirksBodyHtml": ""
+      }
+    },
+    {
+      "data": "<!DOCTYPE HTML PUBLIC'-//W3C//DTD HTML 4.01//EN''http://www.w3.org/TR/html4/strict.dtd'>",
+      "errors": [
+        "(1,21): unexpected-char-in-doctype",
+        "(1,49): unexpected-char-in-doctype"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd\""
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body></body></html>",
+        "noQuirksBodyHtml": ""
+      }
+    }
+  ],
+  "domjs-unsafe.dat": [
+    {
+      "data": "<svg><![CDATA[foo\nbar]]>",
+      "errors": [
+        "(1,5): expected-doctype-but-got-start-tag",
+        "(2,6): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "svg svg": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "svg",
+                    "ns": "http://www.w3.org/2000/svg",
+                    "children": [
+                      {
+                        "text": "foo\nbar"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><svg>foo\nbar</svg></body></html>",
+        "noQuirksBodyHtml": "<svg>foo\nbar</svg>"
+      }
+    },
+    {
+      "data": "<svg><![CDATA[foo\rbar]]>",
+      "errors": [
+        "(1,5): expected-doctype-but-got-start-tag",
+        "(2,6): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "svg svg": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "svg",
+                    "ns": "http://www.w3.org/2000/svg",
+                    "children": [
+                      {
+                        "text": "foo\nbar"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><svg>foo\nbar</svg></body></html>",
+        "noQuirksBodyHtml": "<svg>foo\nbar</svg>"
+      }
+    },
+    {
+      "data": "<svg><![CDATA[foo\r\nbar]]>",
+      "errors": [
+        "(1,5): expected-doctype-but-got-start-tag",
+        "(2,6): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "svg svg": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "svg",
+                    "ns": "http://www.w3.org/2000/svg",
+                    "children": [
+                      {
+                        "text": "foo\nbar"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><svg>foo\nbar</svg></body></html>",
+        "noQuirksBodyHtml": "<svg>foo\nbar</svg>"
+      }
+    },
+    {
+      "data": "<script>a='\u0000'</script>",
+      "errors": [
+        "(1,8): expected-doctype-but-got-start-tag",
+        "(1,12): invalid-codepoint"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "a='�'",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><script>a='�'</script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script>a='�'</script>"
+      }
+    },
+    {
+      "data": "<script type=\"data\"><!--\u0000</script>",
+      "errors": [
+        "(1,20): expected-doctype-but-got-start-tag",
+        "(1,25): invalid-codepoint"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "attrs": [
+                      {
+                        "name": "type",
+                        "value": "data"
+                      }
+                    ],
+                    "children": [
+                      {
+                        "text": "<!--�",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><script type=\"data\"><!--�</script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script type=\"data\"><!--�</script>"
+      }
+    },
+    {
+      "data": "<script type=\"data\"><!--foo\u0000</script>",
+      "errors": [
+        "(1,20): expected-doctype-but-got-start-tag",
+        "(1,28): invalid-codepoint"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "attrs": [
+                      {
+                        "name": "type",
+                        "value": "data"
+                      }
+                    ],
+                    "children": [
+                      {
+                        "text": "<!--foo�",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><script type=\"data\"><!--foo�</script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script type=\"data\"><!--foo�</script>"
+      }
+    },
+    {
+      "data": "<script type=\"data\"><!-- foo-\u0000</script>",
+      "errors": [
+        "(1,20): expected-doctype-but-got-start-tag",
+        "(1,30): invalid-codepoint"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "attrs": [
+                      {
+                        "name": "type",
+                        "value": "data"
+                      }
+                    ],
+                    "children": [
+                      {
+                        "text": "<!-- foo-�",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><script type=\"data\"><!-- foo-�</script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script type=\"data\"><!-- foo-�</script>"
+      }
+    },
+    {
+      "data": "<script type=\"data\"><!-- foo--\u0000</script>",
+      "errors": [
+        "(1,20): expected-doctype-but-got-start-tag",
+        "(1,31): invalid-codepoint"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "attrs": [
+                      {
+                        "name": "type",
+                        "value": "data"
+                      }
+                    ],
+                    "children": [
+                      {
+                        "text": "<!-- foo--�",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><script type=\"data\"><!-- foo--�</script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script type=\"data\"><!-- foo--�</script>"
+      }
+    },
+    {
+      "data": "<script type=\"data\"><!-- foo-",
+      "errors": [
+        "(1,20): expected-doctype-but-got-start-tag",
+        "(1,29): expected-script-data-but-got-eof",
+        "(1,29): expected-named-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "attrs": [
+                      {
+                        "name": "type",
+                        "value": "data"
+                      }
+                    ],
+                    "children": [
+                      {
+                        "text": "<!-- foo-",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><script type=\"data\"><!-- foo-</script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script type=\"data\"><!-- foo-</script>"
+      }
+    },
+    {
+      "data": "<script type=\"data\"><!-- foo-<</script>",
+      "errors": [
+        "(1,20): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "attrs": [
+                      {
+                        "name": "type",
+                        "value": "data"
+                      }
+                    ],
+                    "children": [
+                      {
+                        "text": "<!-- foo-<",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><script type=\"data\"><!-- foo-<</script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script type=\"data\"><!-- foo-<</script>"
+      }
+    },
+    {
+      "data": "<script type=\"data\"><!-- foo-<S",
+      "errors": [
+        "(1,20): expected-doctype-but-got-start-tag",
+        "(1,31): expected-script-data-but-got-eof",
+        "(1,31): expected-named-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "attrs": [
+                      {
+                        "name": "type",
+                        "value": "data"
+                      }
+                    ],
+                    "children": [
+                      {
+                        "text": "<!-- foo-<S",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><script type=\"data\"><!-- foo-<S</script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script type=\"data\"><!-- foo-<S</script>"
+      }
+    },
+    {
+      "data": "<script type=\"data\"><!-- foo-</SCRIPT>",
+      "errors": [
+        "(1,20): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "attrs": [
+                      {
+                        "name": "type",
+                        "value": "data"
+                      }
+                    ],
+                    "children": [
+                      {
+                        "text": "<!-- foo-",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><script type=\"data\"><!-- foo-</script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script type=\"data\"><!-- foo-</script>"
+      }
+    },
+    {
+      "data": "<script type=\"data\"><!--<p></script>",
+      "errors": [
+        "(1,20): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "attrs": [
+                      {
+                        "name": "type",
+                        "value": "data"
+                      }
+                    ],
+                    "children": [
+                      {
+                        "text": "<!--<p>",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><script type=\"data\"><!--<p></script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script type=\"data\"><!--<p></script>"
+      }
+    },
+    {
+      "data": "<script type=\"data\"><!--<script></script></script>",
+      "errors": [
+        "(1,20): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "attrs": [
+                      {
+                        "name": "type",
+                        "value": "data"
+                      }
+                    ],
+                    "children": [
+                      {
+                        "text": "<!--<script></script>",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><script type=\"data\"><!--<script></script></script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script type=\"data\"><!--<script></script></script>"
+      }
+    },
+    {
+      "data": "<script type=\"data\"><!--<script>\u0000</script></script>",
+      "errors": [
+        "(1,20): expected-doctype-but-got-start-tag",
+        "(1,33): invalid-codepoint"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "attrs": [
+                      {
+                        "name": "type",
+                        "value": "data"
+                      }
+                    ],
+                    "children": [
+                      {
+                        "text": "<!--<script>�</script>",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><script type=\"data\"><!--<script>�</script></script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script type=\"data\"><!--<script>�</script></script>"
+      }
+    },
+    {
+      "data": "<script type=\"data\"><!--<script>-\u0000</script></script>",
+      "errors": [
+        "(1,20): expected-doctype-but-got-start-tag",
+        "(1,34): invalid-codepoint"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "attrs": [
+                      {
+                        "name": "type",
+                        "value": "data"
+                      }
+                    ],
+                    "children": [
+                      {
+                        "text": "<!--<script>-�</script>",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><script type=\"data\"><!--<script>-�</script></script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script type=\"data\"><!--<script>-�</script></script>"
+      }
+    },
+    {
+      "data": "<script type=\"data\"><!--<script>--\u0000</script></script>",
+      "errors": [
+        "(1,20): expected-doctype-but-got-start-tag",
+        "(1,35): invalid-codepoint"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "attrs": [
+                      {
+                        "name": "type",
+                        "value": "data"
+                      }
+                    ],
+                    "children": [
+                      {
+                        "text": "<!--<script>--�</script>",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><script type=\"data\"><!--<script>--�</script></script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script type=\"data\"><!--<script>--�</script></script>"
+      }
+    },
+    {
+      "data": "<script type=\"data\"><!--<script>---</script></script>",
+      "errors": [
+        "(1,20): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "attrs": [
+                      {
+                        "name": "type",
+                        "value": "data"
+                      }
+                    ],
+                    "children": [
+                      {
+                        "text": "<!--<script>---</script>",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><script type=\"data\"><!--<script>---</script></script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script type=\"data\"><!--<script>---</script></script>"
+      }
+    },
+    {
+      "data": "<script type=\"data\"><!--<script></scrip></SCRIPT></script>",
+      "errors": [
+        "(1,20): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "attrs": [
+                      {
+                        "name": "type",
+                        "value": "data"
+                      }
+                    ],
+                    "children": [
+                      {
+                        "text": "<!--<script></scrip></SCRIPT>",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><script type=\"data\"><!--<script></scrip></SCRIPT></script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script type=\"data\"><!--<script></scrip></SCRIPT></script>"
+      }
+    },
+    {
+      "data": "<script type=\"data\"><!--<script></scrip </SCRIPT></script>",
+      "errors": [
+        "(1,20): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "attrs": [
+                      {
+                        "name": "type",
+                        "value": "data"
+                      }
+                    ],
+                    "children": [
+                      {
+                        "text": "<!--<script></scrip </SCRIPT>",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><script type=\"data\"><!--<script></scrip </SCRIPT></script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script type=\"data\"><!--<script></scrip </SCRIPT></script>"
+      }
+    },
+    {
+      "data": "<script type=\"data\"><!--<script></scrip/</SCRIPT></script>",
+      "errors": [
+        "(1,20): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "attrs": [
+                      {
+                        "name": "type",
+                        "value": "data"
+                      }
+                    ],
+                    "children": [
+                      {
+                        "text": "<!--<script></scrip/</SCRIPT>",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><script type=\"data\"><!--<script></scrip/</SCRIPT></script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script type=\"data\"><!--<script></scrip/</SCRIPT></script>"
+      }
+    },
+    {
+      "data": "<script type=\"data\"></scrip/></script>",
+      "errors": [
+        "(1,20): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "attrs": [
+                      {
+                        "name": "type",
+                        "value": "data"
+                      }
+                    ],
+                    "children": [
+                      {
+                        "text": "</scrip/>",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><script type=\"data\"></scrip/></script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script type=\"data\"></scrip/></script>"
+      }
+    },
+    {
+      "data": "<script type=\"data\"></scrip ></script>",
+      "errors": [
+        "(1,20): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "attrs": [
+                      {
+                        "name": "type",
+                        "value": "data"
+                      }
+                    ],
+                    "children": [
+                      {
+                        "text": "</scrip >",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><script type=\"data\"></scrip ></script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script type=\"data\"></scrip ></script>"
+      }
+    },
+    {
+      "data": "<script type=\"data\"><!--</scrip></script>",
+      "errors": [
+        "(1,20): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "attrs": [
+                      {
+                        "name": "type",
+                        "value": "data"
+                      }
+                    ],
+                    "children": [
+                      {
+                        "text": "<!--</scrip>",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><script type=\"data\"><!--</scrip></script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script type=\"data\"><!--</scrip></script>"
+      }
+    },
+    {
+      "data": "<script type=\"data\"><!--</scrip </script>",
+      "errors": [
+        "(1,20): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "attrs": [
+                      {
+                        "name": "type",
+                        "value": "data"
+                      }
+                    ],
+                    "children": [
+                      {
+                        "text": "<!--</scrip ",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><script type=\"data\"><!--</scrip </script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script type=\"data\"><!--</scrip </script>"
+      }
+    },
+    {
+      "data": "<script type=\"data\"><!--</scrip/</script>",
+      "errors": [
+        "(1,20): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "attrs": [
+                      {
+                        "name": "type",
+                        "value": "data"
+                      }
+                    ],
+                    "children": [
+                      {
+                        "text": "<!--</scrip/",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><script type=\"data\"><!--</scrip/</script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script type=\"data\"><!--</scrip/</script>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><!DOCTYPE html>",
+      "errors": [
+        "(1,30): unexpected-doctype"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body></body></html>",
+        "noQuirksBodyHtml": ""
+      }
+    },
+    {
+      "data": "<html><!DOCTYPE html>",
+      "errors": [
+        "(1,6): expected-doctype-but-got-start-tag",
+        "(1,21): unexpected-doctype"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body></body></html>",
+        "noQuirksBodyHtml": ""
+      }
+    },
+    {
+      "data": "<html><head><!DOCTYPE html></head>",
+      "errors": [
+        "(1,6): expected-doctype-but-got-start-tag",
+        "(1,27): unexpected-doctype"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body></body></html>",
+        "noQuirksBodyHtml": ""
+      }
+    },
+    {
+      "data": "<html><head></head><!DOCTYPE html>",
+      "errors": [
+        "(1,6): expected-doctype-but-got-start-tag",
+        "(1,34): unexpected-doctype"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body></body></html>",
+        "noQuirksBodyHtml": ""
+      }
+    },
+    {
+      "data": "<body></body><!DOCTYPE html>",
+      "errors": [
+        "(1,6): expected-doctype-but-got-start-tag",
+        "(1,28): unexpected-doctype"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body></body></html>",
+        "noQuirksBodyHtml": ""
+      }
+    },
+    {
+      "data": "<table><!DOCTYPE html></table>",
+      "errors": [
+        "(1,7): expected-doctype-but-got-start-tag",
+        "(1,22): unexpected-doctype"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "table"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><table></table></body></html>",
+        "noQuirksBodyHtml": "<table></table>"
+      }
+    },
+    {
+      "data": "<select><!DOCTYPE html></select>",
+      "errors": [
+        "(1,8): expected-doctype-but-got-start-tag",
+        "(1,23): unexpected-doctype"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "select": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "select"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><select></select></body></html>",
+        "noQuirksBodyHtml": "<select></select>"
+      }
+    },
+    {
+      "data": "<table><colgroup><!DOCTYPE html></colgroup></table>",
+      "errors": [
+        "(1,7): expected-doctype-but-got-start-tag",
+        "(1,32): unexpected-doctype"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true,
+            "colgroup": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "colgroup"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><table><colgroup></colgroup></table></body></html>",
+        "noQuirksBodyHtml": "<table><colgroup></colgroup></table>"
+      }
+    },
+    {
+      "data": "<table><colgroup><!--test--></colgroup></table>",
+      "errors": [
+        "(1,7): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true,
+            "colgroup": true
+          },
+          "comment": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "colgroup",
+                        "children": [
+                          {
+                            "comment": "test"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><table><colgroup><!--test--></colgroup></table></body></html>",
+        "noQuirksBodyHtml": "<table><colgroup><!--test--></colgroup></table>"
+      }
+    },
+    {
+      "data": "<table><colgroup><html></colgroup></table>",
+      "errors": [
+        "(1,7): expected-doctype-but-got-start-tag",
+        "(1,23): non-html-root"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true,
+            "colgroup": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "colgroup"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><table><colgroup></colgroup></table></body></html>",
+        "noQuirksBodyHtml": "<table><colgroup></colgroup></table>"
+      }
+    },
+    {
+      "data": "<table><colgroup> foo</colgroup></table>",
+      "errors": [
+        "(1,7): expected-doctype-but-got-start-tag",
+        "(1,32): foster-parenting-character-in-table",
+        "(1,32): foster-parenting-character-in-table",
+        "(1,32): foster-parenting-character-in-table",
+        "(1,32): unexpected-end-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true,
+            "colgroup": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "foo"
+                  },
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "colgroup",
+                        "children": [
+                          {
+                            "text": " "
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>foo<table><colgroup> </colgroup></table></body></html>",
+        "noQuirksBodyHtml": "foo<table><colgroup> </colgroup></table>"
+      }
+    },
+    {
+      "data": "<select><!--test--></select>",
+      "errors": [
+        "(1,8): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "select": true
+          },
+          "comment": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "select",
+                    "children": [
+                      {
+                        "comment": "test"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><select><!--test--></select></body></html>",
+        "noQuirksBodyHtml": "<select><!--test--></select>"
+      }
+    },
+    {
+      "data": "<select><html></select>",
+      "errors": [
+        "(1,8): expected-doctype-but-got-start-tag",
+        "(1,14): non-html-root"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "select": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "select"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><select></select></body></html>",
+        "noQuirksBodyHtml": "<select></select>"
+      }
+    },
+    {
+      "data": "<frameset><html></frameset>",
+      "errors": [
+        "(1,10): expected-doctype-but-got-start-tag",
+        "(1,16): non-html-root"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "frameset": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "frameset"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><frameset></frameset></html>",
+        "noQuirksBodyHtml": ""
+      }
+    },
+    {
+      "data": "<frameset></frameset><html>",
+      "errors": [
+        "(1,10): expected-doctype-but-got-start-tag",
+        "(1,27): non-html-root"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "frameset": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "frameset"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><frameset></frameset></html>",
+        "noQuirksBodyHtml": ""
+      }
+    },
+    {
+      "data": "<frameset></frameset><!DOCTYPE html>",
+      "errors": [
+        "(1,10): expected-doctype-but-got-start-tag",
+        "(1,36): unexpected-doctype"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "frameset": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "frameset"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><frameset></frameset></html>",
+        "noQuirksBodyHtml": ""
+      }
+    },
+    {
+      "data": "<html><body></body></html><!DOCTYPE html>",
+      "errors": [
+        "(1,6): expected-doctype-but-got-start-tag",
+        "(1,41): unexpected-doctype"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body></body></html>",
+        "noQuirksBodyHtml": ""
+      }
+    },
+    {
+      "data": "<svg><!DOCTYPE html></svg>",
+      "errors": [
+        "(1,5): expected-doctype-but-got-start-tag",
+        "(1,20): unexpected-doctype"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "svg svg": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "svg",
+                    "ns": "http://www.w3.org/2000/svg"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><svg></svg></body></html>",
+        "noQuirksBodyHtml": "<svg></svg>"
+      }
+    },
+    {
+      "data": "<svg><font></font></svg>",
+      "errors": [
+        "(1,5): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "svg svg": true,
+            "svg font": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "svg",
+                    "ns": "http://www.w3.org/2000/svg",
+                    "children": [
+                      {
+                        "tag": "font",
+                        "ns": "http://www.w3.org/2000/svg"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><svg><font></font></svg></body></html>",
+        "noQuirksBodyHtml": "<svg><font></font></svg>"
+      }
+    },
+    {
+      "data": "<svg><font id=foo></font></svg>",
+      "errors": [
+        "(1,5): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "svg svg": true,
+            "svg font": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "svg",
+                    "ns": "http://www.w3.org/2000/svg",
+                    "children": [
+                      {
+                        "tag": "font",
+                        "ns": "http://www.w3.org/2000/svg",
+                        "attrs": [
+                          {
+                            "name": "id",
+                            "value": "foo"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><svg><font id=\"foo\"></font></svg></body></html>",
+        "noQuirksBodyHtml": "<svg><font id=\"foo\"></font></svg>"
+      }
+    },
+    {
+      "data": "<svg><font size=4></font></svg>",
+      "errors": [
+        "(1,5): expected-doctype-but-got-start-tag",
+        "(1,18): unexpected-html-element-in-foreign-content",
+        "(1,31): unexpected-end-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "svg svg": true,
+            "font": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "svg",
+                    "ns": "http://www.w3.org/2000/svg"
+                  },
+                  {
+                    "tag": "font",
+                    "attrs": [
+                      {
+                        "name": "size",
+                        "value": "4"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><svg></svg><font size=\"4\"></font></body></html>",
+        "noQuirksBodyHtml": "<svg><font size=\"4\"></font></svg>"
+      }
+    },
+    {
+      "data": "<svg><font color=red></font></svg>",
+      "errors": [
+        "(1,5): expected-doctype-but-got-start-tag",
+        "(1,21): unexpected-html-element-in-foreign-content",
+        "(1,34): unexpected-end-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "svg svg": true,
+            "font": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "svg",
+                    "ns": "http://www.w3.org/2000/svg"
+                  },
+                  {
+                    "tag": "font",
+                    "attrs": [
+                      {
+                        "name": "color",
+                        "value": "red"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><svg></svg><font color=\"red\"></font></body></html>",
+        "noQuirksBodyHtml": "<svg><font color=\"red\"></font></svg>"
+      }
+    },
+    {
+      "data": "<svg><font font=sans></font></svg>",
+      "errors": [
+        "(1,5): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "svg svg": true,
+            "svg font": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "svg",
+                    "ns": "http://www.w3.org/2000/svg",
+                    "children": [
+                      {
+                        "tag": "font",
+                        "ns": "http://www.w3.org/2000/svg",
+                        "attrs": [
+                          {
+                            "name": "font",
+                            "value": "sans"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><svg><font font=\"sans\"></font></svg></body></html>",
+        "noQuirksBodyHtml": "<svg><font font=\"sans\"></font></svg>"
+      }
+    }
+  ],
+  "entities01.dat": [
+    {
+      "data": "FOO&gt;BAR",
+      "errors": [
+        "(1,3): expected-doctype-but-got-chars"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "escaped": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "FOO>BAR",
+                    "escaped": true
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>FOO&gt;BAR</body></html>",
+        "noQuirksBodyHtml": "FOO&gt;BAR"
+      }
+    },
+    {
+      "data": "FOO&gtBAR",
+      "errors": [
+        "(1,3): expected-doctype-but-got-chars",
+        "(1,6): named-entity-without-semicolon"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "escaped": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "FOO>BAR",
+                    "escaped": true
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>FOO&gt;BAR</body></html>",
+        "noQuirksBodyHtml": "FOO&gt;BAR"
+      }
+    },
+    {
+      "data": "FOO&gt BAR",
+      "errors": [
+        "(1,3): expected-doctype-but-got-chars",
+        "(1,6): named-entity-without-semicolon"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "escaped": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "FOO> BAR",
+                    "escaped": true
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>FOO&gt; BAR</body></html>",
+        "noQuirksBodyHtml": "FOO&gt; BAR"
+      }
+    },
+    {
+      "data": "FOO&gt;;;BAR",
+      "errors": [
+        "(1,3): expected-doctype-but-got-chars"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "escaped": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "FOO>;;BAR",
+                    "escaped": true
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>FOO&gt;;;BAR</body></html>",
+        "noQuirksBodyHtml": "FOO&gt;;;BAR"
+      }
+    },
+    {
+      "data": "I'm &notit; I tell you",
+      "errors": [
+        "(1,4): expected-doctype-but-got-chars",
+        "(1,9): named-entity-without-semicolon"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "I'm ¬it; I tell you"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>I'm ¬it; I tell you</body></html>",
+        "noQuirksBodyHtml": "I'm ¬it; I tell you"
+      }
+    },
+    {
+      "data": "I'm &notin; I tell you",
+      "errors": [
+        "(1,4): expected-doctype-but-got-chars"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "I'm ∉ I tell you"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>I'm ∉ I tell you</body></html>",
+        "noQuirksBodyHtml": "I'm ∉ I tell you"
+      }
+    },
+    {
+      "data": "FOO& BAR",
+      "errors": [
+        "(1,3): expected-doctype-but-got-chars"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "escaped": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "FOO& BAR",
+                    "escaped": true
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>FOO&amp; BAR</body></html>",
+        "noQuirksBodyHtml": "FOO&amp; BAR"
+      }
+    },
+    {
+      "data": "FOO&<BAR>",
+      "errors": [
+        "(1,3): expected-doctype-but-got-chars",
+        "(1,9): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "bar": true
+          },
+          "escaped": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "FOO&",
+                    "escaped": true
+                  },
+                  {
+                    "tag": "bar"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>FOO&amp;<bar></bar></body></html>",
+        "noQuirksBodyHtml": "FOO&amp;<bar></bar>"
+      }
+    },
+    {
+      "data": "FOO&&&&gt;BAR",
+      "errors": [
+        "(1,3): expected-doctype-but-got-chars"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "escaped": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "FOO&&&>BAR",
+                    "escaped": true
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>FOO&amp;&amp;&amp;&gt;BAR</body></html>",
+        "noQuirksBodyHtml": "FOO&amp;&amp;&amp;&gt;BAR"
+      }
+    },
+    {
+      "data": "FOO&#41;BAR",
+      "errors": [
+        "(1,3): expected-doctype-but-got-chars"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "FOO)BAR"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>FOO)BAR</body></html>",
+        "noQuirksBodyHtml": "FOO)BAR"
+      }
+    },
+    {
+      "data": "FOO&#x41;BAR",
+      "errors": [
+        "(1,3): expected-doctype-but-got-chars"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "FOOABAR"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>FOOABAR</body></html>",
+        "noQuirksBodyHtml": "FOOABAR"
+      }
+    },
+    {
+      "data": "FOO&#X41;BAR",
+      "errors": [
+        "(1,3): expected-doctype-but-got-chars"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "FOOABAR"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>FOOABAR</body></html>",
+        "noQuirksBodyHtml": "FOOABAR"
+      }
+    },
+    {
+      "data": "FOO&#BAR",
+      "errors": [
+        "(1,3): expected-doctype-but-got-chars",
+        "(1,5): expected-numeric-entity"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "escaped": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "FOO&#BAR",
+                    "escaped": true
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>FOO&amp;#BAR</body></html>",
+        "noQuirksBodyHtml": "FOO&amp;#BAR"
+      }
+    },
+    {
+      "data": "FOO&#ZOO",
+      "errors": [
+        "(1,3): expected-doctype-but-got-chars",
+        "(1,5): expected-numeric-entity"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "escaped": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "FOO&#ZOO",
+                    "escaped": true
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>FOO&amp;#ZOO</body></html>",
+        "noQuirksBodyHtml": "FOO&amp;#ZOO"
+      }
+    },
+    {
+      "data": "FOO&#xBAR",
+      "errors": [
+        "(1,3): expected-doctype-but-got-chars",
+        "(1,7): expected-numeric-entity"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "FOOºR"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>FOOºR</body></html>",
+        "noQuirksBodyHtml": "FOOºR"
+      }
+    },
+    {
+      "data": "FOO&#xZOO",
+      "errors": [
+        "(1,3): expected-doctype-but-got-chars",
+        "(1,6): expected-numeric-entity"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "escaped": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "FOO&#xZOO",
+                    "escaped": true
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>FOO&amp;#xZOO</body></html>",
+        "noQuirksBodyHtml": "FOO&amp;#xZOO"
+      }
+    },
+    {
+      "data": "FOO&#XZOO",
+      "errors": [
+        "(1,3): expected-doctype-but-got-chars",
+        "(1,6): expected-numeric-entity"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "escaped": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "FOO&#XZOO",
+                    "escaped": true
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>FOO&amp;#XZOO</body></html>",
+        "noQuirksBodyHtml": "FOO&amp;#XZOO"
+      }
+    },
+    {
+      "data": "FOO&#41BAR",
+      "errors": [
+        "(1,3): expected-doctype-but-got-chars",
+        "(1,7): numeric-entity-without-semicolon"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "FOO)BAR"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>FOO)BAR</body></html>",
+        "noQuirksBodyHtml": "FOO)BAR"
+      }
+    },
+    {
+      "data": "FOO&#x41BAR",
+      "errors": [
+        "(1,3): expected-doctype-but-got-chars",
+        "(1,10): numeric-entity-without-semicolon"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "FOO䆺R"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>FOO䆺R</body></html>",
+        "noQuirksBodyHtml": "FOO䆺R"
+      }
+    },
+    {
+      "data": "FOO&#x41ZOO",
+      "errors": [
+        "(1,3): expected-doctype-but-got-chars",
+        "(1,8): numeric-entity-without-semicolon"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "FOOAZOO"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>FOOAZOO</body></html>",
+        "noQuirksBodyHtml": "FOOAZOO"
+      }
+    },
+    {
+      "data": "FOO&#x0000;ZOO",
+      "errors": [
+        "(1,3): expected-doctype-but-got-chars",
+        "(1,11): illegal-codepoint-for-numeric-entity"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "FOO�ZOO"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>FOO�ZOO</body></html>",
+        "noQuirksBodyHtml": "FOO�ZOO"
+      }
+    },
+    {
+      "data": "FOO&#x0078;ZOO",
+      "errors": [
+        "(1,3): expected-doctype-but-got-chars"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "FOOxZOO"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>FOOxZOO</body></html>",
+        "noQuirksBodyHtml": "FOOxZOO"
+      }
+    },
+    {
+      "data": "FOO&#x0079;ZOO",
+      "errors": [
+        "(1,3): expected-doctype-but-got-chars"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "FOOyZOO"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>FOOyZOO</body></html>",
+        "noQuirksBodyHtml": "FOOyZOO"
+      }
+    },
+    {
+      "data": "FOO&#x0080;ZOO",
+      "errors": [
+        "(1,3): expected-doctype-but-got-chars",
+        "(1,11): illegal-codepoint-for-numeric-entity"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "FOO€ZOO"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>FOO€ZOO</body></html>",
+        "noQuirksBodyHtml": "FOO€ZOO"
+      }
+    },
+    {
+      "data": "FOO&#x0081;ZOO",
+      "errors": [
+        "(1,3): expected-doctype-but-got-chars",
+        "(1,11): illegal-codepoint-for-numeric-entity"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "FOO\81ZOO"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>FOO\81ZOO</body></html>",
+        "noQuirksBodyHtml": "FOO\81ZOO"
+      }
+    },
+    {
+      "data": "FOO&#x0082;ZOO",
+      "errors": [
+        "(1,3): expected-doctype-but-got-chars",
+        "(1,11): illegal-codepoint-for-numeric-entity"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "FOO‚ZOO"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>FOO‚ZOO</body></html>",
+        "noQuirksBodyHtml": "FOO‚ZOO"
+      }
+    },
+    {
+      "data": "FOO&#x0083;ZOO",
+      "errors": [
+        "(1,3): expected-doctype-but-got-chars",
+        "(1,11): illegal-codepoint-for-numeric-entity"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "FOOƒZOO"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>FOOƒZOO</body></html>",
+        "noQuirksBodyHtml": "FOOƒZOO"
+      }
+    },
+    {
+      "data": "FOO&#x0084;ZOO",
+      "errors": [
+        "(1,3): expected-doctype-but-got-chars",
+        "(1,11): illegal-codepoint-for-numeric-entity"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "FOO„ZOO"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>FOO„ZOO</body></html>",
+        "noQuirksBodyHtml": "FOO„ZOO"
+      }
+    },
+    {
+      "data": "FOO&#x0085;ZOO",
+      "errors": [
+        "(1,3): expected-doctype-but-got-chars",
+        "(1,11): illegal-codepoint-for-numeric-entity"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "FOO…ZOO"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>FOO…ZOO</body></html>",
+        "noQuirksBodyHtml": "FOO…ZOO"
+      }
+    },
+    {
+      "data": "FOO&#x0086;ZOO",
+      "errors": [
+        "(1,3): expected-doctype-but-got-chars",
+        "(1,11): illegal-codepoint-for-numeric-entity"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "FOO†ZOO"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>FOO†ZOO</body></html>",
+        "noQuirksBodyHtml": "FOO†ZOO"
+      }
+    },
+    {
+      "data": "FOO&#x0087;ZOO",
+      "errors": [
+        "(1,3): expected-doctype-but-got-chars",
+        "(1,11): illegal-codepoint-for-numeric-entity"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "FOO‡ZOO"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>FOO‡ZOO</body></html>",
+        "noQuirksBodyHtml": "FOO‡ZOO"
+      }
+    },
+    {
+      "data": "FOO&#x0088;ZOO",
+      "errors": [
+        "(1,3): expected-doctype-but-got-chars",
+        "(1,11): illegal-codepoint-for-numeric-entity"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "FOOˆZOO"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>FOOˆZOO</body></html>",
+        "noQuirksBodyHtml": "FOOˆZOO"
+      }
+    },
+    {
+      "data": "FOO&#x0089;ZOO",
+      "errors": [
+        "(1,3): expected-doctype-but-got-chars",
+        "(1,11): illegal-codepoint-for-numeric-entity"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "FOO‰ZOO"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>FOO‰ZOO</body></html>",
+        "noQuirksBodyHtml": "FOO‰ZOO"
+      }
+    },
+    {
+      "data": "FOO&#x008A;ZOO",
+      "errors": [
+        "(1,3): expected-doctype-but-got-chars",
+        "(1,11): illegal-codepoint-for-numeric-entity"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "FOOŠZOO"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>FOOŠZOO</body></html>",
+        "noQuirksBodyHtml": "FOOŠZOO"
+      }
+    },
+    {
+      "data": "FOO&#x008B;ZOO",
+      "errors": [
+        "(1,3): expected-doctype-but-got-chars",
+        "(1,11): illegal-codepoint-for-numeric-entity"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "FOO‹ZOO"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>FOO‹ZOO</body></html>",
+        "noQuirksBodyHtml": "FOO‹ZOO"
+      }
+    },
+    {
+      "data": "FOO&#x008C;ZOO",
+      "errors": [
+        "(1,3): expected-doctype-but-got-chars",
+        "(1,11): illegal-codepoint-for-numeric-entity"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "FOOŒZOO"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>FOOŒZOO</body></html>",
+        "noQuirksBodyHtml": "FOOŒZOO"
+      }
+    },
+    {
+      "data": "FOO&#x008D;ZOO",
+      "errors": [
+        "(1,3): expected-doctype-but-got-chars",
+        "(1,11): illegal-codepoint-for-numeric-entity"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "FOO\8dZOO"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>FOO\8dZOO</body></html>",
+        "noQuirksBodyHtml": "FOO\8dZOO"
+      }
+    },
+    {
+      "data": "FOO&#x008E;ZOO",
+      "errors": [
+        "(1,3): expected-doctype-but-got-chars",
+        "(1,11): illegal-codepoint-for-numeric-entity"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "FOOŽZOO"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>FOOŽZOO</body></html>",
+        "noQuirksBodyHtml": "FOOŽZOO"
+      }
+    },
+    {
+      "data": "FOO&#x008F;ZOO",
+      "errors": [
+        "(1,3): expected-doctype-but-got-chars",
+        "(1,11): illegal-codepoint-for-numeric-entity"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "FOO\8fZOO"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>FOO\8fZOO</body></html>",
+        "noQuirksBodyHtml": "FOO\8fZOO"
+      }
+    },
+    {
+      "data": "FOO&#x0090;ZOO",
+      "errors": [
+        "(1,3): expected-doctype-but-got-chars",
+        "(1,11): illegal-codepoint-for-numeric-entity"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "FOO\90ZOO"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>FOO\90ZOO</body></html>",
+        "noQuirksBodyHtml": "FOO\90ZOO"
+      }
+    },
+    {
+      "data": "FOO&#x0091;ZOO",
+      "errors": [
+        "(1,3): expected-doctype-but-got-chars",
+        "(1,11): illegal-codepoint-for-numeric-entity"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "FOO‘ZOO"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>FOO‘ZOO</body></html>",
+        "noQuirksBodyHtml": "FOO‘ZOO"
+      }
+    },
+    {
+      "data": "FOO&#x0092;ZOO",
+      "errors": [
+        "(1,3): expected-doctype-but-got-chars",
+        "(1,11): illegal-codepoint-for-numeric-entity"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "FOO’ZOO"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>FOO’ZOO</body></html>",
+        "noQuirksBodyHtml": "FOO’ZOO"
+      }
+    },
+    {
+      "data": "FOO&#x0093;ZOO",
+      "errors": [
+        "(1,3): expected-doctype-but-got-chars",
+        "(1,11): illegal-codepoint-for-numeric-entity"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "FOO“ZOO"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>FOO“ZOO</body></html>",
+        "noQuirksBodyHtml": "FOO“ZOO"
+      }
+    },
+    {
+      "data": "FOO&#x0094;ZOO",
+      "errors": [
+        "(1,3): expected-doctype-but-got-chars",
+        "(1,11): illegal-codepoint-for-numeric-entity"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "FOO”ZOO"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>FOO”ZOO</body></html>",
+        "noQuirksBodyHtml": "FOO”ZOO"
+      }
+    },
+    {
+      "data": "FOO&#x0095;ZOO",
+      "errors": [
+        "(1,3): expected-doctype-but-got-chars",
+        "(1,11): illegal-codepoint-for-numeric-entity"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "FOO•ZOO"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>FOO•ZOO</body></html>",
+        "noQuirksBodyHtml": "FOO•ZOO"
+      }
+    },
+    {
+      "data": "FOO&#x0096;ZOO",
+      "errors": [
+        "(1,3): expected-doctype-but-got-chars",
+        "(1,11): illegal-codepoint-for-numeric-entity"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "FOO–ZOO"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>FOO–ZOO</body></html>",
+        "noQuirksBodyHtml": "FOO–ZOO"
+      }
+    },
+    {
+      "data": "FOO&#x0097;ZOO",
+      "errors": [
+        "(1,3): expected-doctype-but-got-chars",
+        "(1,11): illegal-codepoint-for-numeric-entity"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "FOO—ZOO"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>FOO—ZOO</body></html>",
+        "noQuirksBodyHtml": "FOO—ZOO"
+      }
+    },
+    {
+      "data": "FOO&#x0098;ZOO",
+      "errors": [
+        "(1,3): expected-doctype-but-got-chars",
+        "(1,11): illegal-codepoint-for-numeric-entity"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "FOO˜ZOO"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>FOO˜ZOO</body></html>",
+        "noQuirksBodyHtml": "FOO˜ZOO"
+      }
+    },
+    {
+      "data": "FOO&#x0099;ZOO",
+      "errors": [
+        "(1,3): expected-doctype-but-got-chars",
+        "(1,11): illegal-codepoint-for-numeric-entity"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "FOO™ZOO"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>FOO™ZOO</body></html>",
+        "noQuirksBodyHtml": "FOO™ZOO"
+      }
+    },
+    {
+      "data": "FOO&#x009A;ZOO",
+      "errors": [
+        "(1,3): expected-doctype-but-got-chars",
+        "(1,11): illegal-codepoint-for-numeric-entity"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "FOOšZOO"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>FOOšZOO</body></html>",
+        "noQuirksBodyHtml": "FOOšZOO"
+      }
+    },
+    {
+      "data": "FOO&#x009B;ZOO",
+      "errors": [
+        "(1,3): expected-doctype-but-got-chars",
+        "(1,11): illegal-codepoint-for-numeric-entity"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "FOO›ZOO"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>FOO›ZOO</body></html>",
+        "noQuirksBodyHtml": "FOO›ZOO"
+      }
+    },
+    {
+      "data": "FOO&#x009C;ZOO",
+      "errors": [
+        "(1,3): expected-doctype-but-got-chars",
+        "(1,11): illegal-codepoint-for-numeric-entity"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "FOOœZOO"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>FOOœZOO</body></html>",
+        "noQuirksBodyHtml": "FOOœZOO"
+      }
+    },
+    {
+      "data": "FOO&#x009D;ZOO",
+      "errors": [
+        "(1,3): expected-doctype-but-got-chars",
+        "(1,11): illegal-codepoint-for-numeric-entity"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "FOO\9dZOO"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>FOO\9dZOO</body></html>",
+        "noQuirksBodyHtml": "FOO\9dZOO"
+      }
+    },
+    {
+      "data": "FOO&#x009E;ZOO",
+      "errors": [
+        "(1,3): expected-doctype-but-got-chars",
+        "(1,11): illegal-codepoint-for-numeric-entity"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "FOOžZOO"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>FOOžZOO</body></html>",
+        "noQuirksBodyHtml": "FOOžZOO"
+      }
+    },
+    {
+      "data": "FOO&#x009F;ZOO",
+      "errors": [
+        "(1,3): expected-doctype-but-got-chars",
+        "(1,11): illegal-codepoint-for-numeric-entity"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "FOOŸZOO"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>FOOŸZOO</body></html>",
+        "noQuirksBodyHtml": "FOOŸZOO"
+      }
+    },
+    {
+      "data": "FOO&#x00A0;ZOO",
+      "errors": [
+        "(1,3): expected-doctype-but-got-chars"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "escaped": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "FOO ZOO",
+                    "escaped": true
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>FOO&nbsp;ZOO</body></html>",
+        "noQuirksBodyHtml": "FOO&nbsp;ZOO"
+      }
+    },
+    {
+      "data": "FOO&#xD7FF;ZOO",
+      "errors": [
+        "(1,3): expected-doctype-but-got-chars"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "FOO퟿ZOO"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>FOO퟿ZOO</body></html>",
+        "noQuirksBodyHtml": "FOO퟿ZOO"
+      }
+    },
+    {
+      "data": "FOO&#xD800;ZOO",
+      "errors": [
+        "(1,3): expected-doctype-but-got-chars",
+        "(1,11): illegal-codepoint-for-numeric-entity"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "FOO�ZOO"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>FOO�ZOO</body></html>",
+        "noQuirksBodyHtml": "FOO�ZOO"
+      }
+    },
+    {
+      "data": "FOO&#xD801;ZOO",
+      "errors": [
+        "(1,3): expected-doctype-but-got-chars",
+        "(1,11): illegal-codepoint-for-numeric-entity"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "FOO�ZOO"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>FOO�ZOO</body></html>",
+        "noQuirksBodyHtml": "FOO�ZOO"
+      }
+    },
+    {
+      "data": "FOO&#xDFFE;ZOO",
+      "errors": [
+        "(1,3): expected-doctype-but-got-chars",
+        "(1,11): illegal-codepoint-for-numeric-entity"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "FOO�ZOO"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>FOO�ZOO</body></html>",
+        "noQuirksBodyHtml": "FOO�ZOO"
+      }
+    },
+    {
+      "data": "FOO&#xDFFF;ZOO",
+      "errors": [
+        "(1,3): expected-doctype-but-got-chars",
+        "(1,11): illegal-codepoint-for-numeric-entity"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "FOO�ZOO"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>FOO�ZOO</body></html>",
+        "noQuirksBodyHtml": "FOO�ZOO"
+      }
+    },
+    {
+      "data": "FOO&#xE000;ZOO",
+      "errors": [
+        "(1,3): expected-doctype-but-got-chars"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "FOOZOO"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>FOOZOO</body></html>",
+        "noQuirksBodyHtml": "FOOZOO"
+      }
+    },
+    {
+      "data": "FOO&#x10FFFE;ZOO",
+      "errors": [
+        "(1,3): expected-doctype-but-got-chars",
+        "(1,13): illegal-codepoint-for-numeric-entity"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "FOO􏿾ZOO"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>FOO􏿾ZOO</body></html>",
+        "noQuirksBodyHtml": "FOO􏿾ZOO"
+      }
+    },
+    {
+      "data": "FOO&#x1087D4;ZOO",
+      "errors": [
+        "(1,3): expected-doctype-but-got-chars"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "FOO􈟔ZOO"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>FOO􈟔ZOO</body></html>",
+        "noQuirksBodyHtml": "FOO􈟔ZOO"
+      }
+    },
+    {
+      "data": "FOO&#x10FFFF;ZOO",
+      "errors": [
+        "(1,3): expected-doctype-but-got-chars",
+        "(1,13): illegal-codepoint-for-numeric-entity"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "FOO􏿿ZOO"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>FOO􏿿ZOO</body></html>",
+        "noQuirksBodyHtml": "FOO􏿿ZOO"
+      }
+    },
+    {
+      "data": "FOO&#x110000;ZOO",
+      "errors": [
+        "(1,3): expected-doctype-but-got-chars",
+        "(1,13): illegal-codepoint-for-numeric-entity"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "FOO�ZOO"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>FOO�ZOO</body></html>",
+        "noQuirksBodyHtml": "FOO�ZOO"
+      }
+    },
+    {
+      "data": "FOO&#xFFFFFF;ZOO",
+      "errors": [
+        "(1,3): expected-doctype-but-got-chars",
+        "(1,13): illegal-codepoint-for-numeric-entity"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "FOO�ZOO"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>FOO�ZOO</body></html>",
+        "noQuirksBodyHtml": "FOO�ZOO"
+      }
+    },
+    {
+      "data": "FOO&#11111111111",
+      "errors": [
+        "(1,3): expected-doctype-but-got-chars",
+        "(1,13): illegal-codepoint-for-numeric-entity",
+        "(1,13): eof-in-numeric-entity"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "FOO�"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>FOO�</body></html>",
+        "noQuirksBodyHtml": "FOO�"
+      }
+    },
+    {
+      "data": "FOO&#1111111111",
+      "errors": [
+        "(1,3): expected-doctype-but-got-chars",
+        "(1,13): illegal-codepoint-for-numeric-entity",
+        "(1,13): eof-in-numeric-entity"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "FOO�"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>FOO�</body></html>",
+        "noQuirksBodyHtml": "FOO�"
+      }
+    },
+    {
+      "data": "FOO&#111111111111",
+      "errors": [
+        "(1,3): expected-doctype-but-got-chars",
+        "(1,13): illegal-codepoint-for-numeric-entity",
+        "(1,13): eof-in-numeric-entity"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "FOO�"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>FOO�</body></html>",
+        "noQuirksBodyHtml": "FOO�"
+      }
+    },
+    {
+      "data": "FOO&#11111111111ZOO",
+      "errors": [
+        "(1,3): expected-doctype-but-got-chars",
+        "(1,16): numeric-entity-without-semicolon",
+        "(1,16): illegal-codepoint-for-numeric-entity"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "FOO�ZOO"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>FOO�ZOO</body></html>",
+        "noQuirksBodyHtml": "FOO�ZOO"
+      }
+    },
+    {
+      "data": "FOO&#1111111111ZOO",
+      "errors": [
+        "(1,3): expected-doctype-but-got-chars",
+        "(1,15): numeric-entity-without-semicolon",
+        "(1,15): illegal-codepoint-for-numeric-entity"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "FOO�ZOO"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>FOO�ZOO</body></html>",
+        "noQuirksBodyHtml": "FOO�ZOO"
+      }
+    },
+    {
+      "data": "FOO&#111111111111ZOO",
+      "errors": [
+        "(1,3): expected-doctype-but-got-chars",
+        "(1,17): numeric-entity-without-semicolon",
+        "(1,17): illegal-codepoint-for-numeric-entity"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "FOO�ZOO"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>FOO�ZOO</body></html>",
+        "noQuirksBodyHtml": "FOO�ZOO"
+      }
+    }
+  ],
+  "entities02.dat": [
+    {
+      "data": "<div bar=\"ZZ&gt;YY\"></div>",
+      "errors": [
+        "(1,20): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "div": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "div",
+                    "attrs": [
+                      {
+                        "name": "bar",
+                        "value": "ZZ>YY"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><div bar=\"ZZ>YY\"></div></body></html>",
+        "noQuirksBodyHtml": "<div bar=\"ZZ>YY\"></div>"
+      }
+    },
+    {
+      "data": "<div bar=\"ZZ&\"></div>",
+      "errors": [
+        "(1,15): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "div": true
+          },
+          "escaped": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "div",
+                    "attrs": [
+                      {
+                        "name": "bar",
+                        "value": "ZZ&",
+                        "escaped": true
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><div bar=\"ZZ&amp;\"></div></body></html>",
+        "noQuirksBodyHtml": "<div bar=\"ZZ&amp;\"></div>"
+      }
+    },
+    {
+      "data": "<div bar='ZZ&'></div>",
+      "errors": [
+        "(1,15): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "div": true
+          },
+          "escaped": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "div",
+                    "attrs": [
+                      {
+                        "name": "bar",
+                        "value": "ZZ&",
+                        "escaped": true
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><div bar=\"ZZ&amp;\"></div></body></html>",
+        "noQuirksBodyHtml": "<div bar=\"ZZ&amp;\"></div>"
+      }
+    },
+    {
+      "data": "<div bar=ZZ&></div>",
+      "errors": [
+        "(1,13): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "div": true
+          },
+          "escaped": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "div",
+                    "attrs": [
+                      {
+                        "name": "bar",
+                        "value": "ZZ&",
+                        "escaped": true
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><div bar=\"ZZ&amp;\"></div></body></html>",
+        "noQuirksBodyHtml": "<div bar=\"ZZ&amp;\"></div>"
+      }
+    },
+    {
+      "data": "<div bar=\"ZZ&gt=YY\"></div>",
+      "errors": [
+        "(1,15): named-entity-without-semicolon",
+        "(1,20): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "div": true
+          },
+          "escaped": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "div",
+                    "attrs": [
+                      {
+                        "name": "bar",
+                        "value": "ZZ&gt=YY",
+                        "escaped": true
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><div bar=\"ZZ&amp;gt=YY\"></div></body></html>",
+        "noQuirksBodyHtml": "<div bar=\"ZZ&amp;gt=YY\"></div>"
+      }
+    },
+    {
+      "data": "<div bar=\"ZZ&gt0YY\"></div>",
+      "errors": [
+        "(1,20): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "div": true
+          },
+          "escaped": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "div",
+                    "attrs": [
+                      {
+                        "name": "bar",
+                        "value": "ZZ&gt0YY",
+                        "escaped": true
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><div bar=\"ZZ&amp;gt0YY\"></div></body></html>",
+        "noQuirksBodyHtml": "<div bar=\"ZZ&amp;gt0YY\"></div>"
+      }
+    },
+    {
+      "data": "<div bar=\"ZZ&gt9YY\"></div>",
+      "errors": [
+        "(1,20): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "div": true
+          },
+          "escaped": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "div",
+                    "attrs": [
+                      {
+                        "name": "bar",
+                        "value": "ZZ&gt9YY",
+                        "escaped": true
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><div bar=\"ZZ&amp;gt9YY\"></div></body></html>",
+        "noQuirksBodyHtml": "<div bar=\"ZZ&amp;gt9YY\"></div>"
+      }
+    },
+    {
+      "data": "<div bar=\"ZZ&gtaYY\"></div>",
+      "errors": [
+        "(1,20): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "div": true
+          },
+          "escaped": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "div",
+                    "attrs": [
+                      {
+                        "name": "bar",
+                        "value": "ZZ&gtaYY",
+                        "escaped": true
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><div bar=\"ZZ&amp;gtaYY\"></div></body></html>",
+        "noQuirksBodyHtml": "<div bar=\"ZZ&amp;gtaYY\"></div>"
+      }
+    },
+    {
+      "data": "<div bar=\"ZZ&gtZYY\"></div>",
+      "errors": [
+        "(1,20): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "div": true
+          },
+          "escaped": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "div",
+                    "attrs": [
+                      {
+                        "name": "bar",
+                        "value": "ZZ&gtZYY",
+                        "escaped": true
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><div bar=\"ZZ&amp;gtZYY\"></div></body></html>",
+        "noQuirksBodyHtml": "<div bar=\"ZZ&amp;gtZYY\"></div>"
+      }
+    },
+    {
+      "data": "<div bar=\"ZZ&gt YY\"></div>",
+      "errors": [
+        "(1,15): named-entity-without-semicolon",
+        "(1,20): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "div": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "div",
+                    "attrs": [
+                      {
+                        "name": "bar",
+                        "value": "ZZ> YY"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><div bar=\"ZZ> YY\"></div></body></html>",
+        "noQuirksBodyHtml": "<div bar=\"ZZ> YY\"></div>"
+      }
+    },
+    {
+      "data": "<div bar=\"ZZ&gt\"></div>",
+      "errors": [
+        "(1,15): named-entity-without-semicolon",
+        "(1,17): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "div": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "div",
+                    "attrs": [
+                      {
+                        "name": "bar",
+                        "value": "ZZ>"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><div bar=\"ZZ>\"></div></body></html>",
+        "noQuirksBodyHtml": "<div bar=\"ZZ>\"></div>"
+      }
+    },
+    {
+      "data": "<div bar='ZZ&gt'></div>",
+      "errors": [
+        "(1,15): named-entity-without-semicolon",
+        "(1,17): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "div": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "div",
+                    "attrs": [
+                      {
+                        "name": "bar",
+                        "value": "ZZ>"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><div bar=\"ZZ>\"></div></body></html>",
+        "noQuirksBodyHtml": "<div bar=\"ZZ>\"></div>"
+      }
+    },
+    {
+      "data": "<div bar=ZZ&gt></div>",
+      "errors": [
+        "(1,14): named-entity-without-semicolon",
+        "(1,15): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "div": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "div",
+                    "attrs": [
+                      {
+                        "name": "bar",
+                        "value": "ZZ>"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><div bar=\"ZZ>\"></div></body></html>",
+        "noQuirksBodyHtml": "<div bar=\"ZZ>\"></div>"
+      }
+    },
+    {
+      "data": "<div bar=\"ZZ&pound_id=23\"></div>",
+      "errors": [
+        "(1,18): named-entity-without-semicolon",
+        "(1,26): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "div": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "div",
+                    "attrs": [
+                      {
+                        "name": "bar",
+                        "value": "ZZ£_id=23"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><div bar=\"ZZ£_id=23\"></div></body></html>",
+        "noQuirksBodyHtml": "<div bar=\"ZZ£_id=23\"></div>"
+      }
+    },
+    {
+      "data": "<div bar=\"ZZ&prod_id=23\"></div>",
+      "errors": [
+        "(1,25): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "div": true
+          },
+          "escaped": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "div",
+                    "attrs": [
+                      {
+                        "name": "bar",
+                        "value": "ZZ&prod_id=23",
+                        "escaped": true
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><div bar=\"ZZ&amp;prod_id=23\"></div></body></html>",
+        "noQuirksBodyHtml": "<div bar=\"ZZ&amp;prod_id=23\"></div>"
+      }
+    },
+    {
+      "data": "<div bar=\"ZZ&pound;_id=23\"></div>",
+      "errors": [
+        "(1,27): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "div": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "div",
+                    "attrs": [
+                      {
+                        "name": "bar",
+                        "value": "ZZ£_id=23"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><div bar=\"ZZ£_id=23\"></div></body></html>",
+        "noQuirksBodyHtml": "<div bar=\"ZZ£_id=23\"></div>"
+      }
+    },
+    {
+      "data": "<div bar=\"ZZ&prod;_id=23\"></div>",
+      "errors": [
+        "(1,26): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "div": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "div",
+                    "attrs": [
+                      {
+                        "name": "bar",
+                        "value": "ZZ∏_id=23"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><div bar=\"ZZ∏_id=23\"></div></body></html>",
+        "noQuirksBodyHtml": "<div bar=\"ZZ∏_id=23\"></div>"
+      }
+    },
+    {
+      "data": "<div bar=\"ZZ&pound=23\"></div>",
+      "errors": [
+        "(1,18): named-entity-without-semicolon",
+        "(1,23): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "div": true
+          },
+          "escaped": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "div",
+                    "attrs": [
+                      {
+                        "name": "bar",
+                        "value": "ZZ&pound=23",
+                        "escaped": true
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><div bar=\"ZZ&amp;pound=23\"></div></body></html>",
+        "noQuirksBodyHtml": "<div bar=\"ZZ&amp;pound=23\"></div>"
+      }
+    },
+    {
+      "data": "<div bar=\"ZZ&prod=23\"></div>",
+      "errors": [
+        "(1,22): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "div": true
+          },
+          "escaped": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "div",
+                    "attrs": [
+                      {
+                        "name": "bar",
+                        "value": "ZZ&prod=23",
+                        "escaped": true
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><div bar=\"ZZ&amp;prod=23\"></div></body></html>",
+        "noQuirksBodyHtml": "<div bar=\"ZZ&amp;prod=23\"></div>"
+      }
+    },
+    {
+      "data": "<div>ZZ&pound_id=23</div>",
+      "errors": [
+        "(1,5): expected-doctype-but-got-start-tag",
+        "(1,13): named-entity-without-semicolon"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "div": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "div",
+                    "children": [
+                      {
+                        "text": "ZZ£_id=23"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><div>ZZ£_id=23</div></body></html>",
+        "noQuirksBodyHtml": "<div>ZZ£_id=23</div>"
+      }
+    },
+    {
+      "data": "<div>ZZ&prod_id=23</div>",
+      "errors": [
+        "(1,5): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "div": true
+          },
+          "escaped": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "div",
+                    "children": [
+                      {
+                        "text": "ZZ&prod_id=23",
+                        "escaped": true
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><div>ZZ&amp;prod_id=23</div></body></html>",
+        "noQuirksBodyHtml": "<div>ZZ&amp;prod_id=23</div>"
+      }
+    },
+    {
+      "data": "<div>ZZ&pound;_id=23</div>",
+      "errors": [
+        "(1,5): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "div": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "div",
+                    "children": [
+                      {
+                        "text": "ZZ£_id=23"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><div>ZZ£_id=23</div></body></html>",
+        "noQuirksBodyHtml": "<div>ZZ£_id=23</div>"
+      }
+    },
+    {
+      "data": "<div>ZZ&prod;_id=23</div>",
+      "errors": [
+        "(1,5): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "div": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "div",
+                    "children": [
+                      {
+                        "text": "ZZ∏_id=23"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><div>ZZ∏_id=23</div></body></html>",
+        "noQuirksBodyHtml": "<div>ZZ∏_id=23</div>"
+      }
+    },
+    {
+      "data": "<div>ZZ&pound=23</div>",
+      "errors": [
+        "(1,5): expected-doctype-but-got-start-tag",
+        "(1,13): named-entity-without-semicolon"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "div": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "div",
+                    "children": [
+                      {
+                        "text": "ZZ£=23"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><div>ZZ£=23</div></body></html>",
+        "noQuirksBodyHtml": "<div>ZZ£=23</div>"
+      }
+    },
+    {
+      "data": "<div>ZZ&prod=23</div>",
+      "errors": [
+        "(1,5): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "div": true
+          },
+          "escaped": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "div",
+                    "children": [
+                      {
+                        "text": "ZZ&prod=23",
+                        "escaped": true
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><div>ZZ&amp;prod=23</div></body></html>",
+        "noQuirksBodyHtml": "<div>ZZ&amp;prod=23</div>"
+      }
+    },
+    {
+      "data": "<div>ZZ&AElig=</div>",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "div": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "div",
+                    "children": [
+                      {
+                        "text": "ZZÆ="
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><div>ZZÆ=</div></body></html>",
+        "noQuirksBodyHtml": "<div>ZZÆ=</div>"
+      }
+    }
+  ],
+  "foreign-fragment.dat": [
+    {
+      "data": "<nobr>X",
+      "errors": [
+        "6: HTML start tag “nobr” in a foreign namespace context.",
+        "7: End of file seen and there were open elements.",
+        "6: Unclosed element “nobr”."
+      ],
+      "fragment": {
+        "name": "path",
+        "ns": "http://www.w3.org/2000/svg"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "svg nobr": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "nobr",
+            "ns": "http://www.w3.org/2000/svg",
+            "children": [
+              {
+                "text": "X"
+              }
+            ]
+          }
+        ],
+        "html": "<nobr>X</nobr>",
+        "noQuirksBodyHtml": "<nobr>X</nobr>"
+      }
+    },
+    {
+      "data": "<font color></font>X",
+      "errors": [
+        "12: HTML start tag “font” in a foreign namespace context."
+      ],
+      "fragment": {
+        "name": "path",
+        "ns": "http://www.w3.org/2000/svg"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "svg font": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "font",
+            "ns": "http://www.w3.org/2000/svg",
+            "attrs": [
+              {
+                "name": "color",
+                "value": ""
+              }
+            ]
+          },
+          {
+            "text": "X"
+          }
+        ],
+        "html": "<font color=\"\"></font>X",
+        "noQuirksBodyHtml": "<font color=\"\"></font>X"
+      }
+    },
+    {
+      "data": "<font></font>X",
+      "errors": [],
+      "fragment": {
+        "name": "path",
+        "ns": "http://www.w3.org/2000/svg"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "svg font": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "font",
+            "ns": "http://www.w3.org/2000/svg"
+          },
+          {
+            "text": "X"
+          }
+        ],
+        "html": "<font></font>X",
+        "noQuirksBodyHtml": "<font></font>X"
+      }
+    },
+    {
+      "data": "<g></path>X",
+      "errors": [
+        "10: End tag “path” did not match the name of the current open element (“g”).",
+        "11: End of file seen and there were open elements.",
+        "3: Unclosed element “g”."
+      ],
+      "fragment": {
+        "name": "path",
+        "ns": "http://www.w3.org/2000/svg"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "svg g": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "g",
+            "ns": "http://www.w3.org/2000/svg",
+            "children": [
+              {
+                "text": "X"
+              }
+            ]
+          }
+        ],
+        "html": "<g>X</g>",
+        "noQuirksBodyHtml": "<g>X</g>"
+      }
+    },
+    {
+      "data": "</path>X",
+      "errors": [
+        "5: Stray end tag “path”."
+      ],
+      "fragment": {
+        "name": "path",
+        "ns": "http://www.w3.org/2000/svg"
+      },
+      "document": {
+        "props": {
+          "tags": {}
+        },
+        "tree": [
+          {
+            "text": "X"
+          }
+        ],
+        "html": "X",
+        "noQuirksBodyHtml": "X"
+      }
+    },
+    {
+      "data": "</foreignObject>X",
+      "errors": [
+        "5: Stray end tag “foreignobject”."
+      ],
+      "fragment": {
+        "name": "foreignObject",
+        "ns": "http://www.w3.org/2000/svg"
+      },
+      "document": {
+        "props": {
+          "tags": {}
+        },
+        "tree": [
+          {
+            "text": "X"
+          }
+        ],
+        "html": "X",
+        "noQuirksBodyHtml": "X"
+      }
+    },
+    {
+      "data": "</desc>X",
+      "errors": [
+        "5: Stray end tag “desc”."
+      ],
+      "fragment": {
+        "name": "desc",
+        "ns": "http://www.w3.org/2000/svg"
+      },
+      "document": {
+        "props": {
+          "tags": {}
+        },
+        "tree": [
+          {
+            "text": "X"
+          }
+        ],
+        "html": "X",
+        "noQuirksBodyHtml": "X"
+      }
+    },
+    {
+      "data": "</title>X",
+      "errors": [
+        "5: Stray end tag “title”."
+      ],
+      "fragment": {
+        "name": "title",
+        "ns": "http://www.w3.org/2000/svg"
+      },
+      "document": {
+        "props": {
+          "tags": {}
+        },
+        "tree": [
+          {
+            "text": "X"
+          }
+        ],
+        "html": "X",
+        "noQuirksBodyHtml": "X"
+      }
+    },
+    {
+      "data": "</svg>X",
+      "errors": [
+        "5: Stray end tag “svg”."
+      ],
+      "fragment": {
+        "name": "svg",
+        "ns": "http://www.w3.org/2000/svg"
+      },
+      "document": {
+        "props": {
+          "tags": {}
+        },
+        "tree": [
+          {
+            "text": "X"
+          }
+        ],
+        "html": "X",
+        "noQuirksBodyHtml": "X"
+      }
+    },
+    {
+      "data": "</mfenced>X",
+      "errors": [
+        "5: Stray end tag “mfenced”."
+      ],
+      "fragment": {
+        "name": "mfenced",
+        "ns": "http://www.w3.org/1998/Math/MathML"
+      },
+      "document": {
+        "props": {
+          "tags": {}
+        },
+        "tree": [
+          {
+            "text": "X"
+          }
+        ],
+        "html": "X",
+        "noQuirksBodyHtml": "X"
+      }
+    },
+    {
+      "data": "</malignmark>X",
+      "errors": [
+        "5: Stray end tag “malignmark”."
+      ],
+      "fragment": {
+        "name": "malignmark",
+        "ns": "http://www.w3.org/1998/Math/MathML"
+      },
+      "document": {
+        "props": {
+          "tags": {}
+        },
+        "tree": [
+          {
+            "text": "X"
+          }
+        ],
+        "html": "X",
+        "noQuirksBodyHtml": "X"
+      }
+    },
+    {
+      "data": "</math>X",
+      "errors": [
+        "5: Stray end tag “math”."
+      ],
+      "fragment": {
+        "name": "math",
+        "ns": "http://www.w3.org/1998/Math/MathML"
+      },
+      "document": {
+        "props": {
+          "tags": {}
+        },
+        "tree": [
+          {
+            "text": "X"
+          }
+        ],
+        "html": "X",
+        "noQuirksBodyHtml": "X"
+      }
+    },
+    {
+      "data": "</annotation-xml>X",
+      "errors": [
+        "5: Stray end tag “annotation-xml”."
+      ],
+      "fragment": {
+        "name": "annotation-xml",
+        "ns": "http://www.w3.org/1998/Math/MathML"
+      },
+      "document": {
+        "props": {
+          "tags": {}
+        },
+        "tree": [
+          {
+            "text": "X"
+          }
+        ],
+        "html": "X",
+        "noQuirksBodyHtml": "X"
+      }
+    },
+    {
+      "data": "</mtext>X",
+      "errors": [
+        "5: Stray end tag “mtext”."
+      ],
+      "fragment": {
+        "name": "mtext",
+        "ns": "http://www.w3.org/1998/Math/MathML"
+      },
+      "document": {
+        "props": {
+          "tags": {}
+        },
+        "tree": [
+          {
+            "text": "X"
+          }
+        ],
+        "html": "X",
+        "noQuirksBodyHtml": "X"
+      }
+    },
+    {
+      "data": "</mi>X",
+      "errors": [
+        "5: Stray end tag “mi”."
+      ],
+      "fragment": {
+        "name": "mi",
+        "ns": "http://www.w3.org/1998/Math/MathML"
+      },
+      "document": {
+        "props": {
+          "tags": {}
+        },
+        "tree": [
+          {
+            "text": "X"
+          }
+        ],
+        "html": "X",
+        "noQuirksBodyHtml": "X"
+      }
+    },
+    {
+      "data": "</mo>X",
+      "errors": [
+        "5: Stray end tag “mo”."
+      ],
+      "fragment": {
+        "name": "mo",
+        "ns": "http://www.w3.org/1998/Math/MathML"
+      },
+      "document": {
+        "props": {
+          "tags": {}
+        },
+        "tree": [
+          {
+            "text": "X"
+          }
+        ],
+        "html": "X",
+        "noQuirksBodyHtml": "X"
+      }
+    },
+    {
+      "data": "</mn>X",
+      "errors": [
+        "5: Stray end tag “mn”."
+      ],
+      "fragment": {
+        "name": "mn",
+        "ns": "http://www.w3.org/1998/Math/MathML"
+      },
+      "document": {
+        "props": {
+          "tags": {}
+        },
+        "tree": [
+          {
+            "text": "X"
+          }
+        ],
+        "html": "X",
+        "noQuirksBodyHtml": "X"
+      }
+    },
+    {
+      "data": "</ms>X",
+      "errors": [
+        "5: Stray end tag “ms”."
+      ],
+      "fragment": {
+        "name": "ms",
+        "ns": "http://www.w3.org/1998/Math/MathML"
+      },
+      "document": {
+        "props": {
+          "tags": {}
+        },
+        "tree": [
+          {
+            "text": "X"
+          }
+        ],
+        "html": "X",
+        "noQuirksBodyHtml": "X"
+      }
+    },
+    {
+      "data": "<b></b><mglyph/><i></i><malignmark/><u></u><ms/>X",
+      "errors": [
+        "51: Self-closing syntax (“/>”) used on a non-void HTML element. Ignoring the slash and treating as a start tag.",
+        "52: End of file seen and there were open elements.",
+        "51: Unclosed element “ms”."
+      ],
+      "fragment": {
+        "name": "ms",
+        "ns": "http://www.w3.org/1998/Math/MathML"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "b": true,
+            "math mglyph": true,
+            "i": true,
+            "math malignmark": true,
+            "u": true,
+            "ms": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "b"
+          },
+          {
+            "tag": "mglyph",
+            "ns": "http://www.w3.org/1998/Math/MathML"
+          },
+          {
+            "tag": "i"
+          },
+          {
+            "tag": "malignmark",
+            "ns": "http://www.w3.org/1998/Math/MathML"
+          },
+          {
+            "tag": "u"
+          },
+          {
+            "tag": "ms",
+            "children": [
+              {
+                "text": "X"
+              }
+            ]
+          }
+        ],
+        "html": "<b></b><mglyph></mglyph><i></i><malignmark></malignmark><u></u><ms>X</ms>",
+        "noQuirksBodyHtml": "<b></b><mglyph><i></i><malignmark><u></u><ms>X</ms></malignmark></mglyph>"
+      }
+    },
+    {
+      "data": "<malignmark></malignmark>",
+      "errors": [],
+      "fragment": {
+        "name": "ms",
+        "ns": "http://www.w3.org/1998/Math/MathML"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "math malignmark": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "malignmark",
+            "ns": "http://www.w3.org/1998/Math/MathML"
+          }
+        ],
+        "html": "<malignmark></malignmark>",
+        "noQuirksBodyHtml": "<malignmark></malignmark>"
+      }
+    },
+    {
+      "data": "<div></div>",
+      "errors": [],
+      "fragment": {
+        "name": "ms",
+        "ns": "http://www.w3.org/1998/Math/MathML"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "div": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "div"
+          }
+        ],
+        "html": "<div></div>",
+        "noQuirksBodyHtml": "<div></div>"
+      }
+    },
+    {
+      "data": "<figure></figure>",
+      "errors": [],
+      "fragment": {
+        "name": "ms",
+        "ns": "http://www.w3.org/1998/Math/MathML"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "figure": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "figure"
+          }
+        ],
+        "html": "<figure></figure>",
+        "noQuirksBodyHtml": "<figure></figure>"
+      }
+    },
+    {
+      "data": "<b></b><mglyph/><i></i><malignmark/><u></u><mn/>X",
+      "errors": [
+        "51: Self-closing syntax (“/>”) used on a non-void HTML element. Ignoring the slash and treating as a start tag.",
+        "52: End of file seen and there were open elements.",
+        "51: Unclosed element “mn”."
+      ],
+      "fragment": {
+        "name": "mn",
+        "ns": "http://www.w3.org/1998/Math/MathML"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "b": true,
+            "math mglyph": true,
+            "i": true,
+            "math malignmark": true,
+            "u": true,
+            "mn": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "b"
+          },
+          {
+            "tag": "mglyph",
+            "ns": "http://www.w3.org/1998/Math/MathML"
+          },
+          {
+            "tag": "i"
+          },
+          {
+            "tag": "malignmark",
+            "ns": "http://www.w3.org/1998/Math/MathML"
+          },
+          {
+            "tag": "u"
+          },
+          {
+            "tag": "mn",
+            "children": [
+              {
+                "text": "X"
+              }
+            ]
+          }
+        ],
+        "html": "<b></b><mglyph></mglyph><i></i><malignmark></malignmark><u></u><mn>X</mn>",
+        "noQuirksBodyHtml": "<b></b><mglyph><i></i><malignmark><u></u><mn>X</mn></malignmark></mglyph>"
+      }
+    },
+    {
+      "data": "<malignmark></malignmark>",
+      "errors": [],
+      "fragment": {
+        "name": "mn",
+        "ns": "http://www.w3.org/1998/Math/MathML"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "math malignmark": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "malignmark",
+            "ns": "http://www.w3.org/1998/Math/MathML"
+          }
+        ],
+        "html": "<malignmark></malignmark>",
+        "noQuirksBodyHtml": "<malignmark></malignmark>"
+      }
+    },
+    {
+      "data": "<div></div>",
+      "errors": [],
+      "fragment": {
+        "name": "mn",
+        "ns": "http://www.w3.org/1998/Math/MathML"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "div": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "div"
+          }
+        ],
+        "html": "<div></div>",
+        "noQuirksBodyHtml": "<div></div>"
+      }
+    },
+    {
+      "data": "<figure></figure>",
+      "errors": [],
+      "fragment": {
+        "name": "mn",
+        "ns": "http://www.w3.org/1998/Math/MathML"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "figure": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "figure"
+          }
+        ],
+        "html": "<figure></figure>",
+        "noQuirksBodyHtml": "<figure></figure>"
+      }
+    },
+    {
+      "data": "<b></b><mglyph/><i></i><malignmark/><u></u><mo/>X",
+      "errors": [
+        "51: Self-closing syntax (“/>”) used on a non-void HTML element. Ignoring the slash and treating as a start tag.",
+        "52: End of file seen and there were open elements.",
+        "51: Unclosed element “mo”."
+      ],
+      "fragment": {
+        "name": "mo",
+        "ns": "http://www.w3.org/1998/Math/MathML"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "b": true,
+            "math mglyph": true,
+            "i": true,
+            "math malignmark": true,
+            "u": true,
+            "mo": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "b"
+          },
+          {
+            "tag": "mglyph",
+            "ns": "http://www.w3.org/1998/Math/MathML"
+          },
+          {
+            "tag": "i"
+          },
+          {
+            "tag": "malignmark",
+            "ns": "http://www.w3.org/1998/Math/MathML"
+          },
+          {
+            "tag": "u"
+          },
+          {
+            "tag": "mo",
+            "children": [
+              {
+                "text": "X"
+              }
+            ]
+          }
+        ],
+        "html": "<b></b><mglyph></mglyph><i></i><malignmark></malignmark><u></u><mo>X</mo>",
+        "noQuirksBodyHtml": "<b></b><mglyph><i></i><malignmark><u></u><mo>X</mo></malignmark></mglyph>"
+      }
+    },
+    {
+      "data": "<malignmark></malignmark>",
+      "errors": [],
+      "fragment": {
+        "name": "mo",
+        "ns": "http://www.w3.org/1998/Math/MathML"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "math malignmark": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "malignmark",
+            "ns": "http://www.w3.org/1998/Math/MathML"
+          }
+        ],
+        "html": "<malignmark></malignmark>",
+        "noQuirksBodyHtml": "<malignmark></malignmark>"
+      }
+    },
+    {
+      "data": "<div></div>",
+      "errors": [],
+      "fragment": {
+        "name": "mo",
+        "ns": "http://www.w3.org/1998/Math/MathML"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "div": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "div"
+          }
+        ],
+        "html": "<div></div>",
+        "noQuirksBodyHtml": "<div></div>"
+      }
+    },
+    {
+      "data": "<figure></figure>",
+      "errors": [],
+      "fragment": {
+        "name": "mo",
+        "ns": "http://www.w3.org/1998/Math/MathML"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "figure": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "figure"
+          }
+        ],
+        "html": "<figure></figure>",
+        "noQuirksBodyHtml": "<figure></figure>"
+      }
+    },
+    {
+      "data": "<b></b><mglyph/><i></i><malignmark/><u></u><mi/>X",
+      "errors": [
+        "51: Self-closing syntax (“/>”) used on a non-void HTML element. Ignoring the slash and treating as a start tag.",
+        "52: End of file seen and there were open elements.",
+        "51: Unclosed element “mi”."
+      ],
+      "fragment": {
+        "name": "mi",
+        "ns": "http://www.w3.org/1998/Math/MathML"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "b": true,
+            "math mglyph": true,
+            "i": true,
+            "math malignmark": true,
+            "u": true,
+            "mi": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "b"
+          },
+          {
+            "tag": "mglyph",
+            "ns": "http://www.w3.org/1998/Math/MathML"
+          },
+          {
+            "tag": "i"
+          },
+          {
+            "tag": "malignmark",
+            "ns": "http://www.w3.org/1998/Math/MathML"
+          },
+          {
+            "tag": "u"
+          },
+          {
+            "tag": "mi",
+            "children": [
+              {
+                "text": "X"
+              }
+            ]
+          }
+        ],
+        "html": "<b></b><mglyph></mglyph><i></i><malignmark></malignmark><u></u><mi>X</mi>",
+        "noQuirksBodyHtml": "<b></b><mglyph><i></i><malignmark><u></u><mi>X</mi></malignmark></mglyph>"
+      }
+    },
+    {
+      "data": "<malignmark></malignmark>",
+      "errors": [],
+      "fragment": {
+        "name": "mi",
+        "ns": "http://www.w3.org/1998/Math/MathML"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "math malignmark": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "malignmark",
+            "ns": "http://www.w3.org/1998/Math/MathML"
+          }
+        ],
+        "html": "<malignmark></malignmark>",
+        "noQuirksBodyHtml": "<malignmark></malignmark>"
+      }
+    },
+    {
+      "data": "<div></div>",
+      "errors": [],
+      "fragment": {
+        "name": "mi",
+        "ns": "http://www.w3.org/1998/Math/MathML"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "div": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "div"
+          }
+        ],
+        "html": "<div></div>",
+        "noQuirksBodyHtml": "<div></div>"
+      }
+    },
+    {
+      "data": "<figure></figure>",
+      "errors": [],
+      "fragment": {
+        "name": "mi",
+        "ns": "http://www.w3.org/1998/Math/MathML"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "figure": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "figure"
+          }
+        ],
+        "html": "<figure></figure>",
+        "noQuirksBodyHtml": "<figure></figure>"
+      }
+    },
+    {
+      "data": "<b></b><mglyph/><i></i><malignmark/><u></u><mtext/>X",
+      "errors": [
+        "51: Self-closing syntax (“/>”) used on a non-void HTML element. Ignoring the slash and treating as a start tag.",
+        "52: End of file seen and there were open elements.",
+        "51: Unclosed element “mtext”."
+      ],
+      "fragment": {
+        "name": "mtext",
+        "ns": "http://www.w3.org/1998/Math/MathML"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "b": true,
+            "math mglyph": true,
+            "i": true,
+            "math malignmark": true,
+            "u": true,
+            "mtext": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "b"
+          },
+          {
+            "tag": "mglyph",
+            "ns": "http://www.w3.org/1998/Math/MathML"
+          },
+          {
+            "tag": "i"
+          },
+          {
+            "tag": "malignmark",
+            "ns": "http://www.w3.org/1998/Math/MathML"
+          },
+          {
+            "tag": "u"
+          },
+          {
+            "tag": "mtext",
+            "children": [
+              {
+                "text": "X"
+              }
+            ]
+          }
+        ],
+        "html": "<b></b><mglyph></mglyph><i></i><malignmark></malignmark><u></u><mtext>X</mtext>",
+        "noQuirksBodyHtml": "<b></b><mglyph><i></i><malignmark><u></u><mtext>X</mtext></malignmark></mglyph>"
+      }
+    },
+    {
+      "data": "<malignmark></malignmark>",
+      "errors": [],
+      "fragment": {
+        "name": "mtext",
+        "ns": "http://www.w3.org/1998/Math/MathML"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "math malignmark": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "malignmark",
+            "ns": "http://www.w3.org/1998/Math/MathML"
+          }
+        ],
+        "html": "<malignmark></malignmark>",
+        "noQuirksBodyHtml": "<malignmark></malignmark>"
+      }
+    },
+    {
+      "data": "<div></div>",
+      "errors": [],
+      "fragment": {
+        "name": "mtext",
+        "ns": "http://www.w3.org/1998/Math/MathML"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "div": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "div"
+          }
+        ],
+        "html": "<div></div>",
+        "noQuirksBodyHtml": "<div></div>"
+      }
+    },
+    {
+      "data": "<figure></figure>",
+      "errors": [],
+      "fragment": {
+        "name": "mtext",
+        "ns": "http://www.w3.org/1998/Math/MathML"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "figure": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "figure"
+          }
+        ],
+        "html": "<figure></figure>",
+        "noQuirksBodyHtml": "<figure></figure>"
+      }
+    },
+    {
+      "data": "<div></div>",
+      "errors": [
+        "5: HTML start tag “div” in a foreign namespace context."
+      ],
+      "fragment": {
+        "name": "annotation-xml",
+        "ns": "http://www.w3.org/1998/Math/MathML"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "math div": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "div",
+            "ns": "http://www.w3.org/1998/Math/MathML"
+          }
+        ],
+        "html": "<div></div>",
+        "noQuirksBodyHtml": "<div></div>"
+      }
+    },
+    {
+      "data": "<figure></figure>",
+      "errors": [],
+      "fragment": {
+        "name": "annotation-xml",
+        "ns": "http://www.w3.org/1998/Math/MathML"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "math figure": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "figure",
+            "ns": "http://www.w3.org/1998/Math/MathML"
+          }
+        ],
+        "html": "<figure></figure>",
+        "noQuirksBodyHtml": "<figure></figure>"
+      }
+    },
+    {
+      "data": "<div></div>",
+      "errors": [
+        "5: HTML start tag “div” in a foreign namespace context."
+      ],
+      "fragment": {
+        "name": "math",
+        "ns": "http://www.w3.org/1998/Math/MathML"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "math div": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "div",
+            "ns": "http://www.w3.org/1998/Math/MathML"
+          }
+        ],
+        "html": "<div></div>",
+        "noQuirksBodyHtml": "<div></div>"
+      }
+    },
+    {
+      "data": "<figure></figure>",
+      "errors": [],
+      "fragment": {
+        "name": "math",
+        "ns": "http://www.w3.org/1998/Math/MathML"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "math figure": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "figure",
+            "ns": "http://www.w3.org/1998/Math/MathML"
+          }
+        ],
+        "html": "<figure></figure>",
+        "noQuirksBodyHtml": "<figure></figure>"
+      }
+    },
+    {
+      "data": "<div></div>",
+      "errors": [],
+      "fragment": {
+        "name": "foreignObject",
+        "ns": "http://www.w3.org/2000/svg"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "div": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "div"
+          }
+        ],
+        "html": "<div></div>",
+        "noQuirksBodyHtml": "<div></div>"
+      }
+    },
+    {
+      "data": "<figure></figure>",
+      "errors": [],
+      "fragment": {
+        "name": "foreignObject",
+        "ns": "http://www.w3.org/2000/svg"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "figure": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "figure"
+          }
+        ],
+        "html": "<figure></figure>",
+        "noQuirksBodyHtml": "<figure></figure>"
+      }
+    },
+    {
+      "data": "<div></div>",
+      "errors": [],
+      "fragment": {
+        "name": "title",
+        "ns": "http://www.w3.org/2000/svg"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "div": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "div"
+          }
+        ],
+        "html": "<div></div>",
+        "noQuirksBodyHtml": "<div></div>"
+      }
+    },
+    {
+      "data": "<figure></figure>",
+      "errors": [],
+      "fragment": {
+        "name": "title",
+        "ns": "http://www.w3.org/2000/svg"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "figure": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "figure"
+          }
+        ],
+        "html": "<figure></figure>",
+        "noQuirksBodyHtml": "<figure></figure>"
+      }
+    },
+    {
+      "data": "<figure></figure>",
+      "errors": [],
+      "fragment": {
+        "name": "desc",
+        "ns": "http://www.w3.org/2000/svg"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "figure": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "figure"
+          }
+        ],
+        "html": "<figure></figure>",
+        "noQuirksBodyHtml": "<figure></figure>"
+      }
+    },
+    {
+      "data": "<div><h1>X</h1></div>",
+      "errors": [
+        "5: HTML start tag “div” in a foreign namespace context.",
+        "9: HTML start tag “h1” in a foreign namespace context."
+      ],
+      "fragment": {
+        "name": "svg",
+        "ns": "http://www.w3.org/2000/svg"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "svg div": true,
+            "svg h1": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "div",
+            "ns": "http://www.w3.org/2000/svg",
+            "children": [
+              {
+                "tag": "h1",
+                "ns": "http://www.w3.org/2000/svg",
+                "children": [
+                  {
+                    "text": "X"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<div><h1>X</h1></div>",
+        "noQuirksBodyHtml": "<div><h1>X</h1></div>"
+      }
+    },
+    {
+      "data": "<div></div>",
+      "errors": [
+        "5: HTML start tag “div” in a foreign namespace context."
+      ],
+      "fragment": {
+        "name": "svg",
+        "ns": "http://www.w3.org/2000/svg"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "svg div": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "div",
+            "ns": "http://www.w3.org/2000/svg"
+          }
+        ],
+        "html": "<div></div>",
+        "noQuirksBodyHtml": "<div></div>"
+      }
+    },
+    {
+      "data": "<div></div>",
+      "errors": [],
+      "fragment": {
+        "name": "desc",
+        "ns": "http://www.w3.org/2000/svg"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "div": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "div"
+          }
+        ],
+        "html": "<div></div>",
+        "noQuirksBodyHtml": "<div></div>"
+      }
+    },
+    {
+      "data": "<figure></figure>",
+      "errors": [],
+      "fragment": {
+        "name": "desc",
+        "ns": "http://www.w3.org/2000/svg"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "figure": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "figure"
+          }
+        ],
+        "html": "<figure></figure>",
+        "noQuirksBodyHtml": "<figure></figure>"
+      }
+    },
+    {
+      "data": "<plaintext><foo>",
+      "errors": [
+        "(1,16): expected-closing-tag-but-got-eof"
+      ],
+      "fragment": {
+        "name": "desc",
+        "ns": "http://www.w3.org/2000/svg"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "plaintext": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "plaintext",
+            "children": [
+              {
+                "text": "<foo>",
+                "no_escape": true
+              }
+            ]
+          }
+        ],
+        "html": "<plaintext><foo></plaintext>",
+        "noQuirksBodyHtml": "<plaintext><foo></plaintext>"
+      }
+    },
+    {
+      "data": "<frameset>X",
+      "errors": [
+        "6: Stray start tag “frameset”."
+      ],
+      "fragment": {
+        "name": "desc",
+        "ns": "http://www.w3.org/2000/svg"
+      },
+      "document": {
+        "props": {
+          "tags": {}
+        },
+        "tree": [
+          {
+            "text": "X"
+          }
+        ],
+        "html": "X",
+        "noQuirksBodyHtml": "X"
+      }
+    },
+    {
+      "data": "<head>X",
+      "errors": [
+        "6: Stray start tag “head”."
+      ],
+      "fragment": {
+        "name": "desc",
+        "ns": "http://www.w3.org/2000/svg"
+      },
+      "document": {
+        "props": {
+          "tags": {}
+        },
+        "tree": [
+          {
+            "text": "X"
+          }
+        ],
+        "html": "X",
+        "noQuirksBodyHtml": "X"
+      }
+    },
+    {
+      "data": "<body>X",
+      "errors": [
+        "6: Stray start tag “body”."
+      ],
+      "fragment": {
+        "name": "desc",
+        "ns": "http://www.w3.org/2000/svg"
+      },
+      "document": {
+        "props": {
+          "tags": {}
+        },
+        "tree": [
+          {
+            "text": "X"
+          }
+        ],
+        "html": "X",
+        "noQuirksBodyHtml": "X"
+      }
+    },
+    {
+      "data": "<html>X",
+      "errors": [
+        "6: Stray start tag “html”."
+      ],
+      "fragment": {
+        "name": "desc",
+        "ns": "http://www.w3.org/2000/svg"
+      },
+      "document": {
+        "props": {
+          "tags": {}
+        },
+        "tree": [
+          {
+            "text": "X"
+          }
+        ],
+        "html": "X",
+        "noQuirksBodyHtml": "X"
+      }
+    },
+    {
+      "data": "<html class=\"foo\">X",
+      "errors": [
+        "6: Stray start tag “html”."
+      ],
+      "fragment": {
+        "name": "desc",
+        "ns": "http://www.w3.org/2000/svg"
+      },
+      "document": {
+        "props": {
+          "tags": {}
+        },
+        "tree": [
+          {
+            "text": "X"
+          }
+        ],
+        "html": "X",
+        "noQuirksBodyHtml": "X"
+      }
+    },
+    {
+      "data": "<body class=\"foo\">X",
+      "errors": [
+        "6: Stray start tag “body”."
+      ],
+      "fragment": {
+        "name": "desc",
+        "ns": "http://www.w3.org/2000/svg"
+      },
+      "document": {
+        "props": {
+          "tags": {}
+        },
+        "tree": [
+          {
+            "text": "X"
+          }
+        ],
+        "html": "X",
+        "noQuirksBodyHtml": "X"
+      }
+    }
+  ],
+  "html5test-com.dat": [
+    {
+      "data": "<div<div>",
+      "errors": [
+        "(1,9): expected-doctype-but-got-start-tag",
+        "(1,9): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "div<div": true
+          },
+          "tagWithLt": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "div<div"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><div<div></div<div></body></html>",
+        "noQuirksBodyHtml": "<div<div></div<div>"
+      }
+    },
+    {
+      "data": "<div foo<bar=''>",
+      "errors": [
+        "(1,9): invalid-character-in-attribute-name",
+        "(1,16): expected-doctype-but-got-start-tag",
+        "(1,16): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "div": true
+          },
+          "attrWithFunnyChar": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "div",
+                    "attrs": [
+                      {
+                        "name": "foo<bar",
+                        "value": ""
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><div foo<bar=\"\"></div></body></html>",
+        "noQuirksBodyHtml": "<div foo<bar=\"\"></div>"
+      }
+    },
+    {
+      "data": "<div foo=`bar`>",
+      "errors": [
+        "(1,10): equals-in-unquoted-attribute-value",
+        "(1,14): unexpected-character-in-unquoted-attribute-value",
+        "(1,15): expected-doctype-but-got-start-tag",
+        "(1,15): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "div": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "div",
+                    "attrs": [
+                      {
+                        "name": "foo",
+                        "value": "`bar`"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><div foo=\"`bar`\"></div></body></html>",
+        "noQuirksBodyHtml": "<div foo=\"`bar`\"></div>"
+      }
+    },
+    {
+      "data": "<div \\\"foo=''>",
+      "errors": [
+        "(1,7): invalid-character-in-attribute-name",
+        "(1,14): expected-doctype-but-got-start-tag",
+        "(1,14): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "div": true
+          },
+          "attrWithFunnyChar": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "div",
+                    "attrs": [
+                      {
+                        "name": "\\\"foo",
+                        "value": ""
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><div \\\"foo=\"\"></div></body></html>",
+        "noQuirksBodyHtml": "<div \\\"foo=\"\"></div>"
+      }
+    },
+    {
+      "data": "<a href='\\nbar'></a>",
+      "errors": [
+        "(1,16): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "a": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "a",
+                    "attrs": [
+                      {
+                        "name": "href",
+                        "value": "\\nbar"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><a href=\"\\nbar\"></a></body></html>",
+        "noQuirksBodyHtml": "<a href=\"\\nbar\"></a>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html>",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body></body></html>",
+        "noQuirksBodyHtml": ""
+      }
+    },
+    {
+      "data": "&lang;&rang;",
+      "errors": [
+        "(1,6): expected-doctype-but-got-chars"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "⟨⟩"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>⟨⟩</body></html>",
+        "noQuirksBodyHtml": "⟨⟩"
+      }
+    },
+    {
+      "data": "&apos;",
+      "errors": [
+        "(1,6): expected-doctype-but-got-chars"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "'"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>'</body></html>",
+        "noQuirksBodyHtml": "'"
+      }
+    },
+    {
+      "data": "&ImaginaryI;",
+      "errors": [
+        "(1,12): expected-doctype-but-got-chars"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "ⅈ"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>ⅈ</body></html>",
+        "noQuirksBodyHtml": "ⅈ"
+      }
+    },
+    {
+      "data": "&Kopf;",
+      "errors": [
+        "(1,6): expected-doctype-but-got-chars"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "𝕂"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>𝕂</body></html>",
+        "noQuirksBodyHtml": "𝕂"
+      }
+    },
+    {
+      "data": "&notinva;",
+      "errors": [
+        "(1,9): expected-doctype-but-got-chars"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "∉"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>∉</body></html>",
+        "noQuirksBodyHtml": "∉"
+      }
+    },
+    {
+      "data": "<?import namespace=\"foo\" implementation=\"#bar\">",
+      "errors": [
+        "(1,1): expected-tag-name-but-got-question-mark",
+        "(1,47): expected-doctype-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "comment": true
+        },
+        "tree": [
+          {
+            "comment": "?import namespace=\"foo\" implementation=\"#bar\""
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!--?import namespace=\"foo\" implementation=\"#bar\"--><html><head></head><body></body></html>",
+        "noQuirksBodyHtml": "<!--?import namespace=\"foo\" implementation=\"#bar\"-->"
+      }
+    },
+    {
+      "data": "<!--foo--bar-->",
+      "errors": [
+        "(1,10): unexpected-char-in-comment",
+        "(1,15): expected-doctype-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "comment": true
+        },
+        "tree": [
+          {
+            "comment": "foo--bar"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!--foo--bar--><html><head></head><body></body></html>",
+        "noQuirksBodyHtml": "<!--foo--bar-->"
+      }
+    },
+    {
+      "data": "<![CDATA[x]]>",
+      "errors": [
+        "(1,2): expected-dashes-or-doctype",
+        "(1,13): expected-doctype-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "comment": true
+        },
+        "tree": [
+          {
+            "comment": "[CDATA[x]]"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!--[CDATA[x]]--><html><head></head><body></body></html>",
+        "noQuirksBodyHtml": "<!--[CDATA[x]]-->"
+      }
+    },
+    {
+      "data": "<textarea><!--</textarea>--></textarea>",
+      "errors": [
+        "(1,10): expected-doctype-but-got-start-tag",
+        "(1,39): unexpected-end-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "textarea": true
+          },
+          "escaped": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "textarea",
+                    "children": [
+                      {
+                        "text": "<!--",
+                        "escaped": true
+                      }
+                    ]
+                  },
+                  {
+                    "text": "-->",
+                    "escaped": true
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><textarea>&lt;!--</textarea>--&gt;</body></html>",
+        "noQuirksBodyHtml": "<textarea>&lt;!--</textarea>--&gt;"
+      }
+    },
+    {
+      "data": "<textarea><!--</textarea>-->",
+      "errors": [
+        "(1,10): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "textarea": true
+          },
+          "escaped": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "textarea",
+                    "children": [
+                      {
+                        "text": "<!--",
+                        "escaped": true
+                      }
+                    ]
+                  },
+                  {
+                    "text": "-->",
+                    "escaped": true
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><textarea>&lt;!--</textarea>--&gt;</body></html>",
+        "noQuirksBodyHtml": "<textarea>&lt;!--</textarea>--&gt;"
+      }
+    },
+    {
+      "data": "<style><!--</style>--></style>",
+      "errors": [
+        "(1,7): expected-doctype-but-got-start-tag",
+        "(1,30): unexpected-end-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "style": true,
+            "body": true
+          },
+          "no_escape": true,
+          "escaped": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "style",
+                    "children": [
+                      {
+                        "text": "<!--",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "-->",
+                    "escaped": true
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><style><!--</style></head><body>--&gt;</body></html>",
+        "noQuirksBodyHtml": "<style><!--</style>--&gt;"
+      }
+    },
+    {
+      "data": "<style><!--</style>-->",
+      "errors": [
+        "(1,7): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "style": true,
+            "body": true
+          },
+          "no_escape": true,
+          "escaped": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "style",
+                    "children": [
+                      {
+                        "text": "<!--",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "-->",
+                    "escaped": true
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><style><!--</style></head><body>--&gt;</body></html>",
+        "noQuirksBodyHtml": "<style><!--</style>--&gt;"
+      }
+    },
+    {
+      "data": "<ul><li>A </li> <li>B</li></ul>",
+      "errors": [
+        "(1,4): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "ul": true,
+            "li": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "ul",
+                    "children": [
+                      {
+                        "tag": "li",
+                        "children": [
+                          {
+                            "text": "A "
+                          }
+                        ]
+                      },
+                      {
+                        "text": " "
+                      },
+                      {
+                        "tag": "li",
+                        "children": [
+                          {
+                            "text": "B"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><ul><li>A </li> <li>B</li></ul></body></html>",
+        "noQuirksBodyHtml": "<ul><li>A </li> <li>B</li></ul>"
+      }
+    },
+    {
+      "data": "<table><form><input type=hidden><input></form><div></div></table>",
+      "errors": [
+        "(1,7): expected-doctype-but-got-start-tag",
+        "(1,13): unexpected-form-in-table",
+        "(1,32): unexpected-hidden-input-in-table",
+        "(1,39): unexpected-start-tag-implies-table-voodoo",
+        "(1,46): unexpected-end-tag-implies-table-voodoo",
+        "(1,46): unexpected-end-tag",
+        "(1,51): unexpected-start-tag-implies-table-voodoo",
+        "(1,57): unexpected-end-tag-implies-table-voodoo"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "input": true,
+            "div": true,
+            "table": true,
+            "form": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "input"
+                  },
+                  {
+                    "tag": "div"
+                  },
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "form"
+                      },
+                      {
+                        "tag": "input",
+                        "attrs": [
+                          {
+                            "name": "type",
+                            "value": "hidden"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><input><div></div><table><form></form><input type=\"hidden\"></table></body></html>",
+        "noQuirksBodyHtml": "<input><div></div><table><form></form><input type=\"hidden\"></table>"
+      }
+    },
+    {
+      "data": "<i>A<b>B<p></i>C</b>D",
+      "errors": [
+        "(1,3): expected-doctype-but-got-start-tag",
+        "(1,15): adoption-agency-1.3",
+        "(1,20): adoption-agency-1.3"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "i": true,
+            "b": true,
+            "p": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "i",
+                    "children": [
+                      {
+                        "text": "A"
+                      },
+                      {
+                        "tag": "b",
+                        "children": [
+                          {
+                            "text": "B"
+                          }
+                        ]
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "b"
+                  },
+                  {
+                    "tag": "p",
+                    "children": [
+                      {
+                        "tag": "b",
+                        "children": [
+                          {
+                            "tag": "i"
+                          },
+                          {
+                            "text": "C"
+                          }
+                        ]
+                      },
+                      {
+                        "text": "D"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><i>A<b>B</b></i><b></b><p><b><i></i>C</b>D</p></body></html>",
+        "noQuirksBodyHtml": "<i>A<b>B</b></i><b></b><p><b><i></i>C</b>D</p>"
+      }
+    },
+    {
+      "data": "<div></div>",
+      "errors": [
+        "(1,5): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "div": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "div"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><div></div></body></html>",
+        "noQuirksBodyHtml": "<div></div>"
+      }
+    },
+    {
+      "data": "<svg></svg>",
+      "errors": [
+        "(1,5): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "svg svg": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "svg",
+                    "ns": "http://www.w3.org/2000/svg"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><svg></svg></body></html>",
+        "noQuirksBodyHtml": "<svg></svg>"
+      }
+    },
+    {
+      "data": "<math></math>",
+      "errors": [
+        "(1,6): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "math math": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "math",
+                    "ns": "http://www.w3.org/1998/Math/MathML"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><math></math></body></html>",
+        "noQuirksBodyHtml": "<math></math>"
+      }
+    }
+  ],
+  "inbody01.dat": [
+    {
+      "data": "<button>1</foo>",
+      "errors": [
+        "(1,8): expected-doctype-but-got-start-tag",
+        "(1,15): unexpected-end-tag",
+        "(1,15): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "button": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "button",
+                    "children": [
+                      {
+                        "text": "1"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><button>1</button></body></html>",
+        "noQuirksBodyHtml": "<button>1</button>"
+      }
+    },
+    {
+      "data": "<foo>1<p>2</foo>",
+      "errors": [
+        "(1,5): expected-doctype-but-got-start-tag",
+        "(1,16): unexpected-end-tag",
+        "(1,16): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "foo": true,
+            "p": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "foo",
+                    "children": [
+                      {
+                        "text": "1"
+                      },
+                      {
+                        "tag": "p",
+                        "children": [
+                          {
+                            "text": "2"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><foo>1<p>2</p></foo></body></html>",
+        "noQuirksBodyHtml": "<foo>1<p>2</p></foo>"
+      }
+    },
+    {
+      "data": "<dd>1</foo>",
+      "errors": [
+        "(1,4): expected-doctype-but-got-start-tag",
+        "(1,11): unexpected-end-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "dd": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "dd",
+                    "children": [
+                      {
+                        "text": "1"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><dd>1</dd></body></html>",
+        "noQuirksBodyHtml": "<dd>1</dd>"
+      }
+    },
+    {
+      "data": "<foo>1<dd>2</foo>",
+      "errors": [
+        "(1,5): expected-doctype-but-got-start-tag",
+        "(1,17): unexpected-end-tag",
+        "(1,17): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "foo": true,
+            "dd": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "foo",
+                    "children": [
+                      {
+                        "text": "1"
+                      },
+                      {
+                        "tag": "dd",
+                        "children": [
+                          {
+                            "text": "2"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><foo>1<dd>2</dd></foo></body></html>",
+        "noQuirksBodyHtml": "<foo>1<dd>2</dd></foo>"
+      }
+    }
+  ],
+  "isindex.dat": [
+    {
+      "data": "<isindex>",
+      "errors": [
+        "(1,9): expected-doctype-but-got-start-tag",
+        "(1,9): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "isindex": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "isindex"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><isindex></isindex></body></html>",
+        "noQuirksBodyHtml": "<isindex></isindex>"
+      }
+    },
+    {
+      "data": "<isindex name=\"A\" action=\"B\" prompt=\"C\" foo=\"D\">",
+      "errors": [
+        "(1,48): expected-doctype-but-got-start-tag",
+        "(1,48): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "isindex": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "isindex",
+                    "attrs": [
+                      {
+                        "name": "action",
+                        "value": "B"
+                      },
+                      {
+                        "name": "foo",
+                        "value": "D"
+                      },
+                      {
+                        "name": "name",
+                        "value": "A"
+                      },
+                      {
+                        "name": "prompt",
+                        "value": "C"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><isindex name=\"A\" action=\"B\" prompt=\"C\" foo=\"D\"></isindex></body></html>",
+        "noQuirksBodyHtml": "<isindex name=\"A\" action=\"B\" prompt=\"C\" foo=\"D\"></isindex>"
+      }
+    },
+    {
+      "data": "<form><isindex>",
+      "errors": [
+        "(1,6): expected-doctype-but-got-start-tag",
+        "(1,15): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "form": true,
+            "isindex": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "form",
+                    "children": [
+                      {
+                        "tag": "isindex"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><form><isindex></isindex></form></body></html>",
+        "noQuirksBodyHtml": "<form><isindex></isindex></form>"
+      }
+    },
+    {
+      "data": "<!doctype html><isindex>x</isindex>x",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "isindex": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "isindex",
+                    "children": [
+                      {
+                        "text": "x"
+                      }
+                    ]
+                  },
+                  {
+                    "text": "x"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><isindex>x</isindex>x</body></html>",
+        "noQuirksBodyHtml": "<isindex>x</isindex>x"
+      }
+    }
+  ],
+  "main-element.dat": [
+    {
+      "data": "<!doctype html><p>foo<main>bar<p>baz",
+      "errors": [
+        "(1,36): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "p": true,
+            "main": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "p",
+                    "children": [
+                      {
+                        "text": "foo"
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "main",
+                    "children": [
+                      {
+                        "text": "bar"
+                      },
+                      {
+                        "tag": "p",
+                        "children": [
+                          {
+                            "text": "baz"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><p>foo</p><main>bar<p>baz</p></main></body></html>",
+        "noQuirksBodyHtml": "<p>foo</p><main>bar<p>baz</p></main>"
+      }
+    },
+    {
+      "data": "<!doctype html><main><p>foo</main>bar",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "main": true,
+            "p": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "main",
+                    "children": [
+                      {
+                        "tag": "p",
+                        "children": [
+                          {
+                            "text": "foo"
+                          }
+                        ]
+                      }
+                    ]
+                  },
+                  {
+                    "text": "bar"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><main><p>foo</p></main>bar</body></html>",
+        "noQuirksBodyHtml": "<main><p>foo</p></main>bar"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html>xxx<svg><x><g><a><main><b>",
+      "errors": [
+        " * (1,42) unexpected HTML-like start tag token in foreign content",
+        " * (1,42) unexpected end of file"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "svg svg": true,
+            "svg x": true,
+            "svg g": true,
+            "svg a": true,
+            "svg main": true,
+            "b": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "xxx"
+                  },
+                  {
+                    "tag": "svg",
+                    "ns": "http://www.w3.org/2000/svg",
+                    "children": [
+                      {
+                        "tag": "x",
+                        "ns": "http://www.w3.org/2000/svg",
+                        "children": [
+                          {
+                            "tag": "g",
+                            "ns": "http://www.w3.org/2000/svg",
+                            "children": [
+                              {
+                                "tag": "a",
+                                "ns": "http://www.w3.org/2000/svg",
+                                "children": [
+                                  {
+                                    "tag": "main",
+                                    "ns": "http://www.w3.org/2000/svg"
+                                  }
+                                ]
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "b"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body>xxx<svg><x><g><a><main></main></a></g></x></svg><b></b></body></html>",
+        "noQuirksBodyHtml": "xxx<svg><x><g><a><main><b></b></main></a></g></x></svg>"
+      }
+    }
+  ],
+  "math.dat": [
+    {
+      "data": "<math><tr><td><mo><tr>",
+      "errors": [],
+      "fragment": {
+        "name": "td"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "math math": true,
+            "math tr": true,
+            "math td": true,
+            "math mo": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "math",
+            "ns": "http://www.w3.org/1998/Math/MathML",
+            "children": [
+              {
+                "tag": "tr",
+                "ns": "http://www.w3.org/1998/Math/MathML",
+                "children": [
+                  {
+                    "tag": "td",
+                    "ns": "http://www.w3.org/1998/Math/MathML",
+                    "children": [
+                      {
+                        "tag": "mo",
+                        "ns": "http://www.w3.org/1998/Math/MathML"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<math><tr><td><mo></mo></td></tr></math>",
+        "noQuirksBodyHtml": "<math><tr><td><mo></mo></td></tr></math>"
+      }
+    },
+    {
+      "data": "<math><tr><td><mo><tr>",
+      "errors": [],
+      "fragment": {
+        "name": "tr"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "math math": true,
+            "math tr": true,
+            "math td": true,
+            "math mo": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "math",
+            "ns": "http://www.w3.org/1998/Math/MathML",
+            "children": [
+              {
+                "tag": "tr",
+                "ns": "http://www.w3.org/1998/Math/MathML",
+                "children": [
+                  {
+                    "tag": "td",
+                    "ns": "http://www.w3.org/1998/Math/MathML",
+                    "children": [
+                      {
+                        "tag": "mo",
+                        "ns": "http://www.w3.org/1998/Math/MathML"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<math><tr><td><mo></mo></td></tr></math>",
+        "noQuirksBodyHtml": "<math><tr><td><mo></mo></td></tr></math>"
+      }
+    },
+    {
+      "data": "<math><thead><mo><tbody>",
+      "errors": [],
+      "fragment": {
+        "name": "thead"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "math math": true,
+            "math thead": true,
+            "math mo": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "math",
+            "ns": "http://www.w3.org/1998/Math/MathML",
+            "children": [
+              {
+                "tag": "thead",
+                "ns": "http://www.w3.org/1998/Math/MathML",
+                "children": [
+                  {
+                    "tag": "mo",
+                    "ns": "http://www.w3.org/1998/Math/MathML"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<math><thead><mo></mo></thead></math>",
+        "noQuirksBodyHtml": "<math><thead><mo></mo></thead></math>"
+      }
+    },
+    {
+      "data": "<math><tfoot><mo><tbody>",
+      "errors": [],
+      "fragment": {
+        "name": "tfoot"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "math math": true,
+            "math tfoot": true,
+            "math mo": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "math",
+            "ns": "http://www.w3.org/1998/Math/MathML",
+            "children": [
+              {
+                "tag": "tfoot",
+                "ns": "http://www.w3.org/1998/Math/MathML",
+                "children": [
+                  {
+                    "tag": "mo",
+                    "ns": "http://www.w3.org/1998/Math/MathML"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<math><tfoot><mo></mo></tfoot></math>",
+        "noQuirksBodyHtml": "<math><tfoot><mo></mo></tfoot></math>"
+      }
+    },
+    {
+      "data": "<math><tbody><mo><tfoot>",
+      "errors": [],
+      "fragment": {
+        "name": "tbody"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "math math": true,
+            "math tbody": true,
+            "math mo": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "math",
+            "ns": "http://www.w3.org/1998/Math/MathML",
+            "children": [
+              {
+                "tag": "tbody",
+                "ns": "http://www.w3.org/1998/Math/MathML",
+                "children": [
+                  {
+                    "tag": "mo",
+                    "ns": "http://www.w3.org/1998/Math/MathML"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<math><tbody><mo></mo></tbody></math>",
+        "noQuirksBodyHtml": "<math><tbody><mo></mo></tbody></math>"
+      }
+    },
+    {
+      "data": "<math><tbody><mo></table>",
+      "errors": [],
+      "fragment": {
+        "name": "tbody"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "math math": true,
+            "math tbody": true,
+            "math mo": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "math",
+            "ns": "http://www.w3.org/1998/Math/MathML",
+            "children": [
+              {
+                "tag": "tbody",
+                "ns": "http://www.w3.org/1998/Math/MathML",
+                "children": [
+                  {
+                    "tag": "mo",
+                    "ns": "http://www.w3.org/1998/Math/MathML"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<math><tbody><mo></mo></tbody></math>",
+        "noQuirksBodyHtml": "<math><tbody><mo></mo></tbody></math>"
+      }
+    },
+    {
+      "data": "<math><thead><mo></table>",
+      "errors": [],
+      "fragment": {
+        "name": "tbody"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "math math": true,
+            "math thead": true,
+            "math mo": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "math",
+            "ns": "http://www.w3.org/1998/Math/MathML",
+            "children": [
+              {
+                "tag": "thead",
+                "ns": "http://www.w3.org/1998/Math/MathML",
+                "children": [
+                  {
+                    "tag": "mo",
+                    "ns": "http://www.w3.org/1998/Math/MathML"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<math><thead><mo></mo></thead></math>",
+        "noQuirksBodyHtml": "<math><thead><mo></mo></thead></math>"
+      }
+    },
+    {
+      "data": "<math><tfoot><mo></table>",
+      "errors": [],
+      "fragment": {
+        "name": "tbody"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "math math": true,
+            "math tfoot": true,
+            "math mo": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "math",
+            "ns": "http://www.w3.org/1998/Math/MathML",
+            "children": [
+              {
+                "tag": "tfoot",
+                "ns": "http://www.w3.org/1998/Math/MathML",
+                "children": [
+                  {
+                    "tag": "mo",
+                    "ns": "http://www.w3.org/1998/Math/MathML"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<math><tfoot><mo></mo></tfoot></math>",
+        "noQuirksBodyHtml": "<math><tfoot><mo></mo></tfoot></math>"
+      }
+    }
+  ],
+  "menuitem-element.dat": [
+    {
+      "data": "<menuitem>",
+      "errors": [
+        "10: Start tag seen without seeing a doctype first. Expected “<!DOCTYPE html>”."
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "menuitem": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "menuitem"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><menuitem></menuitem></body></html>",
+        "noQuirksBodyHtml": "<menuitem></menuitem>"
+      }
+    },
+    {
+      "data": "</menuitem>",
+      "errors": [
+        "11: End tag seen without seeing a doctype first. Expected “<!DOCTYPE html>”.",
+        "11: Stray end tag “menuitem”."
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body></body></html>",
+        "noQuirksBodyHtml": ""
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><body><menuitem>A",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "menuitem": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "menuitem",
+                    "children": [
+                      {
+                        "text": "A"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><menuitem>A</menuitem></body></html>",
+        "noQuirksBodyHtml": "<menuitem>A</menuitem>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><body><menuitem>A<menuitem>B",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "menuitem": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "menuitem",
+                    "children": [
+                      {
+                        "text": "A"
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "menuitem",
+                    "children": [
+                      {
+                        "text": "B"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><menuitem>A</menuitem><menuitem>B</menuitem></body></html>",
+        "noQuirksBodyHtml": "<menuitem>A</menuitem><menuitem>B</menuitem>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><body><menuitem>A<menu>B</menu>",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "menuitem": true,
+            "menu": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "menuitem",
+                    "children": [
+                      {
+                        "text": "A"
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "menu",
+                    "children": [
+                      {
+                        "text": "B"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><menuitem>A</menuitem><menu>B</menu></body></html>",
+        "noQuirksBodyHtml": "<menuitem>A</menuitem><menu>B</menu>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><body><menuitem>A<hr>B",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "menuitem": true,
+            "hr": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "menuitem",
+                    "children": [
+                      {
+                        "text": "A"
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "hr"
+                  },
+                  {
+                    "text": "B"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><menuitem>A</menuitem><hr>B</body></html>",
+        "noQuirksBodyHtml": "<menuitem>A</menuitem><hr>B"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><li><menuitem><li>",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "li": true,
+            "menuitem": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "li",
+                    "children": [
+                      {
+                        "tag": "menuitem"
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "li"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><li><menuitem></menuitem></li><li></li></body></html>",
+        "noQuirksBodyHtml": "<li><menuitem></menuitem></li><li></li>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><menuitem><p></menuitem>x",
+      "errors": [
+        "39: Stray end tag “menuitem”."
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "menuitem": true,
+            "p": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "menuitem",
+                    "children": [
+                      {
+                        "tag": "p",
+                        "children": [
+                          {
+                            "text": "x"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><menuitem><p>x</p></menuitem></body></html>",
+        "noQuirksBodyHtml": "<menuitem><p>x</p></menuitem>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><p><b></p><menuitem>",
+      "errors": [
+        "25: End tag “p” seen, but there were open elements.",
+        "21: Unclosed element “b”.",
+        "35: End of file seen and there were open elements."
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "p": true,
+            "b": true,
+            "menuitem": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "p",
+                    "children": [
+                      {
+                        "tag": "b"
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "b",
+                    "children": [
+                      {
+                        "tag": "menuitem"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><p><b></b></p><b><menuitem></menuitem></b></body></html>",
+        "noQuirksBodyHtml": "<p><b></b></p><b><menuitem></menuitem></b>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><menuitem><asdf></menuitem>x",
+      "errors": [
+        "40: End tag “menuitem” seen, but there were open elements.",
+        "31: Unclosed element “asdf”."
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "menuitem": true,
+            "asdf": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "menuitem",
+                    "children": [
+                      {
+                        "tag": "asdf"
+                      }
+                    ]
+                  },
+                  {
+                    "text": "x"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><menuitem><asdf></asdf></menuitem>x</body></html>",
+        "noQuirksBodyHtml": "<menuitem><asdf></asdf></menuitem>x"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html></menuitem>",
+      "errors": [
+        "26: Stray end tag “menuitem”."
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body></body></html>",
+        "noQuirksBodyHtml": ""
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><html></menuitem>",
+      "errors": [
+        "26: Stray end tag “menuitem”."
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body></body></html>",
+        "noQuirksBodyHtml": ""
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><head></menuitem>",
+      "errors": [
+        "26: Stray end tag “menuitem”."
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body></body></html>",
+        "noQuirksBodyHtml": ""
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><select><menuitem></select>",
+      "errors": [
+        "33: Stray start tag “menuitem”."
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "select": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "select"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><select></select></body></html>",
+        "noQuirksBodyHtml": "<select></select>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><option><menuitem>",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "option": true,
+            "menuitem": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "option",
+                    "children": [
+                      {
+                        "tag": "menuitem"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><option><menuitem></menuitem></option></body></html>",
+        "noQuirksBodyHtml": "<option><menuitem></menuitem></option>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><menuitem><option>",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "menuitem": true,
+            "option": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "menuitem",
+                    "children": [
+                      {
+                        "tag": "option"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><menuitem><option></option></menuitem></body></html>",
+        "noQuirksBodyHtml": "<menuitem><option></option></menuitem>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><menuitem></body>",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "menuitem": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "menuitem"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><menuitem></menuitem></body></html>",
+        "noQuirksBodyHtml": "<menuitem></menuitem>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><menuitem></html>",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "menuitem": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "menuitem"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><menuitem></menuitem></body></html>",
+        "noQuirksBodyHtml": "<menuitem></menuitem>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><menuitem><p>",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "menuitem": true,
+            "p": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "menuitem",
+                    "children": [
+                      {
+                        "tag": "p"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><menuitem><p></p></menuitem></body></html>",
+        "noQuirksBodyHtml": "<menuitem><p></p></menuitem>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><menuitem><li>",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "menuitem": true,
+            "li": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "menuitem",
+                    "children": [
+                      {
+                        "tag": "li"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><menuitem><li></li></menuitem></body></html>",
+        "noQuirksBodyHtml": "<menuitem><li></li></menuitem>"
+      }
+    }
+  ],
+  "namespace-sensitivity.dat": [
+    {
+      "data": "<body><table><tr><td><svg><td><foreignObject><span></td>Foo",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true,
+            "tbody": true,
+            "tr": true,
+            "td": true,
+            "svg svg": true,
+            "svg td": true,
+            "svg foreignObject": true,
+            "span": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "Foo"
+                  },
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "tbody",
+                        "children": [
+                          {
+                            "tag": "tr",
+                            "children": [
+                              {
+                                "tag": "td",
+                                "children": [
+                                  {
+                                    "tag": "svg",
+                                    "ns": "http://www.w3.org/2000/svg",
+                                    "children": [
+                                      {
+                                        "tag": "td",
+                                        "ns": "http://www.w3.org/2000/svg",
+                                        "children": [
+                                          {
+                                            "tag": "foreignObject",
+                                            "ns": "http://www.w3.org/2000/svg",
+                                            "children": [
+                                              {
+                                                "tag": "span"
+                                              }
+                                            ]
+                                          }
+                                        ]
+                                      }
+                                    ]
+                                  }
+                                ]
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>Foo<table><tbody><tr><td><svg><td><foreignObject><span></span></foreignObject></td></svg></td></tr></tbody></table></body></html>",
+        "noQuirksBodyHtml": "Foo<table><tbody><tr><td><svg><td><foreignObject><span></span></foreignObject></td></svg></td></tr></tbody></table>"
+      }
+    }
+  ],
+  "noscript01.dat": [
+    {
+      "data": "<head><noscript><!doctype html><!--foo--></noscript>",
+      "errors": [
+        "Line: 1 Col: 6 Unexpected start tag (head). Expected DOCTYPE.",
+        "Line: 1 Col: 31 Unexpected DOCTYPE. Ignored."
+      ],
+      "script": "off",
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "noscript": true,
+            "body": true
+          },
+          "comment": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "noscript",
+                    "children": [
+                      {
+                        "comment": "foo"
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><noscript><!--foo--></noscript></head><body></body></html>",
+        "noQuirksBodyHtml": "<noscript><!--foo--></noscript>"
+      }
+    },
+    {
+      "data": "<head><noscript><html class=\"foo\"><!--foo--></noscript>",
+      "errors": [
+        "Line: 1 Col: 6 Unexpected start tag (head). Expected DOCTYPE.",
+        "Line: 1 Col: 34 html needs to be the first start tag."
+      ],
+      "script": "off",
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "noscript": true,
+            "body": true
+          },
+          "comment": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "attrs": [
+              {
+                "name": "class",
+                "value": "foo"
+              }
+            ],
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "noscript",
+                    "children": [
+                      {
+                        "comment": "foo"
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html class=\"foo\"><head><noscript><!--foo--></noscript></head><body></body></html>",
+        "noQuirksBodyHtml": "<noscript><!--foo--></noscript>"
+      }
+    },
+    {
+      "data": "<head><noscript></noscript>",
+      "errors": [
+        "(1,6): expected-doctype-but-got-tag"
+      ],
+      "script": "off",
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "noscript": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "noscript"
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><noscript></noscript></head><body></body></html>",
+        "noQuirksBodyHtml": "<noscript></noscript>"
+      }
+    },
+    {
+      "data": "<head><noscript>   </noscript>",
+      "errors": [
+        "Line: 1 Col: 6 Unexpected start tag (head). Expected DOCTYPE."
+      ],
+      "script": "off",
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "noscript": true,
+            "body": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "noscript",
+                    "children": [
+                      {
+                        "text": "   ",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><noscript>   </noscript></head><body></body></html>",
+        "noQuirksBodyHtml": "<noscript>   </noscript>"
+      }
+    },
+    {
+      "data": "<head><noscript><!--foo--></noscript>",
+      "errors": [
+        "(1,6): expected-doctype-but-got-tag"
+      ],
+      "script": "off",
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "noscript": true,
+            "body": true
+          },
+          "comment": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "noscript",
+                    "children": [
+                      {
+                        "comment": "foo"
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><noscript><!--foo--></noscript></head><body></body></html>",
+        "noQuirksBodyHtml": "<noscript><!--foo--></noscript>"
+      }
+    },
+    {
+      "data": "<head><noscript><basefont><!--foo--></noscript>",
+      "errors": [
+        "Line: 1 Col: 6 Unexpected start tag (head). Expected DOCTYPE."
+      ],
+      "script": "off",
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "noscript": true,
+            "basefont": true,
+            "body": true
+          },
+          "comment": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "noscript",
+                    "children": [
+                      {
+                        "tag": "basefont"
+                      },
+                      {
+                        "comment": "foo"
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><noscript><basefont><!--foo--></noscript></head><body></body></html>",
+        "noQuirksBodyHtml": "<noscript><basefont><!--foo--></noscript>"
+      }
+    },
+    {
+      "data": "<head><noscript><bgsound><!--foo--></noscript>",
+      "errors": [
+        "Line: 1 Col: 6 Unexpected start tag (head). Expected DOCTYPE."
+      ],
+      "script": "off",
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "noscript": true,
+            "bgsound": true,
+            "body": true
+          },
+          "comment": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "noscript",
+                    "children": [
+                      {
+                        "tag": "bgsound"
+                      },
+                      {
+                        "comment": "foo"
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><noscript><bgsound><!--foo--></noscript></head><body></body></html>",
+        "noQuirksBodyHtml": "<noscript><bgsound><!--foo--></noscript>"
+      }
+    },
+    {
+      "data": "<head><noscript><link><!--foo--></noscript>",
+      "errors": [
+        "Line: 1 Col: 6 Unexpected start tag (head). Expected DOCTYPE."
+      ],
+      "script": "off",
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "noscript": true,
+            "link": true,
+            "body": true
+          },
+          "comment": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "noscript",
+                    "children": [
+                      {
+                        "tag": "link"
+                      },
+                      {
+                        "comment": "foo"
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><noscript><link><!--foo--></noscript></head><body></body></html>",
+        "noQuirksBodyHtml": "<noscript><link><!--foo--></noscript>"
+      }
+    },
+    {
+      "data": "<head><noscript><meta><!--foo--></noscript>",
+      "errors": [
+        "Line: 1 Col: 6 Unexpected start tag (head). Expected DOCTYPE."
+      ],
+      "script": "off",
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "noscript": true,
+            "meta": true,
+            "body": true
+          },
+          "comment": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "noscript",
+                    "children": [
+                      {
+                        "tag": "meta"
+                      },
+                      {
+                        "comment": "foo"
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><noscript><meta><!--foo--></noscript></head><body></body></html>",
+        "noQuirksBodyHtml": "<noscript><meta><!--foo--></noscript>"
+      }
+    },
+    {
+      "data": "<head><noscript><noframes>XXX</noscript></noframes></noscript>",
+      "errors": [
+        "Line: 1 Col: 6 Unexpected start tag (head). Expected DOCTYPE."
+      ],
+      "script": "off",
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "noscript": true,
+            "noframes": true,
+            "body": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "noscript",
+                    "children": [
+                      {
+                        "tag": "noframes",
+                        "children": [
+                          {
+                            "text": "XXX</noscript>",
+                            "no_escape": true
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><noscript><noframes>XXX</noscript></noframes></noscript></head><body></body></html>",
+        "noQuirksBodyHtml": "<noscript><noframes>XXX</noscript></noframes></noscript>"
+      }
+    },
+    {
+      "data": "<head><noscript><style>XXX</style></noscript>",
+      "errors": [
+        "Line: 1 Col: 6 Unexpected start tag (head). Expected DOCTYPE."
+      ],
+      "script": "off",
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "noscript": true,
+            "style": true,
+            "body": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "noscript",
+                    "children": [
+                      {
+                        "tag": "style",
+                        "children": [
+                          {
+                            "text": "XXX",
+                            "no_escape": true
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><noscript><style>XXX</style></noscript></head><body></body></html>",
+        "noQuirksBodyHtml": "<noscript><style>XXX</style></noscript>"
+      }
+    },
+    {
+      "data": "<head><noscript></br><!--foo--></noscript>",
+      "errors": [
+        "Line: 1 Col: 6 Unexpected start tag (head). Expected DOCTYPE.",
+        "Line: 1 Col: 21 Element br not allowed in a inhead-noscript context",
+        "Line: 1 Col: 21 Unexpected end tag (br). Treated as br element.",
+        "Line: 1 Col: 42 Unexpected end tag (noscript). Ignored."
+      ],
+      "script": "off",
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "noscript": true,
+            "body": true,
+            "br": true
+          },
+          "comment": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "noscript"
+                  }
+                ]
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "br"
+                  },
+                  {
+                    "comment": "foo"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><noscript></noscript></head><body><br><!--foo--></body></html>",
+        "noQuirksBodyHtml": "<noscript><br><!--foo--></noscript>"
+      }
+    },
+    {
+      "data": "<head><noscript><head class=\"foo\"><!--foo--></noscript>",
+      "errors": [
+        "Line: 1 Col: 6 Unexpected start tag (head). Expected DOCTYPE.",
+        "Line: 1 Col: 34 Unexpected start tag (head)."
+      ],
+      "script": "off",
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "noscript": true,
+            "body": true
+          },
+          "comment": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "noscript",
+                    "children": [
+                      {
+                        "comment": "foo"
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><noscript><!--foo--></noscript></head><body></body></html>",
+        "noQuirksBodyHtml": "<noscript><!--foo--></noscript>"
+      }
+    },
+    {
+      "data": "<head><noscript><noscript class=\"foo\"><!--foo--></noscript>",
+      "errors": [
+        "Line: 1 Col: 6 Unexpected start tag (head). Expected DOCTYPE.",
+        "Line: 1 Col: 34 Unexpected start tag (noscript)."
+      ],
+      "script": "off",
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "noscript": true,
+            "body": true
+          },
+          "comment": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "noscript",
+                    "children": [
+                      {
+                        "comment": "foo"
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><noscript><!--foo--></noscript></head><body></body></html>",
+        "noQuirksBodyHtml": "<noscript><noscript class=\"foo\"><!--foo--></noscript></noscript>"
+      }
+    },
+    {
+      "data": "<head><noscript></p><!--foo--></noscript>",
+      "errors": [
+        "Line: 1 Col: 6 Unexpected start tag (head). Expected DOCTYPE.",
+        "Line: 1 Col: 20 Unexpected end tag (p). Ignored."
+      ],
+      "script": "off",
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "noscript": true,
+            "body": true
+          },
+          "comment": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "noscript",
+                    "children": [
+                      {
+                        "comment": "foo"
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><noscript><!--foo--></noscript></head><body></body></html>",
+        "noQuirksBodyHtml": "<noscript><p></p><!--foo--></noscript>"
+      }
+    },
+    {
+      "data": "<head><noscript><p><!--foo--></noscript>",
+      "errors": [
+        "Line: 1 Col: 6 Unexpected start tag (head). Expected DOCTYPE.",
+        "Line: 1 Col: 19 Element p not allowed in a inhead-noscript context",
+        "Line: 1 Col: 40 Unexpected end tag (noscript). Ignored."
+      ],
+      "script": "off",
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "noscript": true,
+            "body": true,
+            "p": true
+          },
+          "comment": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "noscript"
+                  }
+                ]
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "p",
+                    "children": [
+                      {
+                        "comment": "foo"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><noscript></noscript></head><body><p><!--foo--></p></body></html>",
+        "noQuirksBodyHtml": "<noscript><p><!--foo--></p></noscript>"
+      }
+    },
+    {
+      "data": "<head><noscript>XXX<!--foo--></noscript></head>",
+      "errors": [
+        "Line: 1 Col: 6 Unexpected start tag (head). Expected DOCTYPE.",
+        "Line: 1 Col: 19 Unexpected non-space character. Expected inhead-noscript content",
+        "Line: 1 Col: 30 Unexpected end tag (noscript). Ignored.",
+        "Line: 1 Col: 37 Unexpected end tag (head). Ignored."
+      ],
+      "script": "off",
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "noscript": true,
+            "body": true
+          },
+          "comment": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "noscript"
+                  }
+                ]
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "XXX"
+                  },
+                  {
+                    "comment": "foo"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><noscript></noscript></head><body>XXX<!--foo--></body></html>",
+        "noQuirksBodyHtml": "<noscript>XXX<!--foo--></noscript>"
+      }
+    },
+    {
+      "data": "<head><noscript>",
+      "errors": [
+        "(1,6): expected-doctype-but-got-tag",
+        "(1,6): eof-in-head-noscript"
+      ],
+      "script": "off",
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "noscript": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "noscript"
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><noscript></noscript></head><body></body></html>",
+        "noQuirksBodyHtml": "<noscript></noscript>"
+      }
+    }
+  ],
+  "pending-spec-changes-plain-text-unsafe.dat": [
+    {
+      "data": "<body><table>\u0000filler\u0000text\u0000",
+      "errors": [
+        "(1,6): expected-doctype-but-got-start-tag",
+        "(1,14): invalid-codepoint",
+        "(1,14): invalid-codepoint-in-table-text",
+        "(1,21): invalid-codepoint",
+        "(1,21): invalid-codepoint-in-table-text",
+        "(1,26): invalid-codepoint",
+        "(1,26): invalid-codepoint-in-table-text",
+        "(1,26): foster-parenting-character-in-table",
+        "(1,26): foster-parenting-character-in-table",
+        "(1,26): foster-parenting-character-in-table",
+        "(1,26): foster-parenting-character-in-table",
+        "(1,26): foster-parenting-character-in-table",
+        "(1,26): foster-parenting-character-in-table",
+        "(1,26): foster-parenting-character-in-table",
+        "(1,26): foster-parenting-character-in-table",
+        "(1,26): foster-parenting-character-in-table",
+        "(1,26): foster-parenting-character-in-table",
+        "(1,26): eof-in-table"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "fillertext"
+                  },
+                  {
+                    "tag": "table"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>fillertext<table></table></body></html>",
+        "noQuirksBodyHtml": "fillertext<table></table>"
+      }
+    }
+  ],
+  "pending-spec-changes.dat": [
+    {
+      "data": "<input type=\"hidden\"><frameset>",
+      "errors": [
+        "(1,21): expected-doctype-but-got-start-tag",
+        "(1,31): unexpected-start-tag",
+        "(1,31): eof-in-frameset"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "frameset": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "frameset"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><frameset></frameset></html>",
+        "noQuirksBodyHtml": "<input type=\"hidden\">"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><table><caption><svg>foo</table>bar",
+      "errors": [
+        "(1,47): unexpected-end-tag",
+        "(1,47): end-table-tag-in-caption"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true,
+            "caption": true,
+            "svg svg": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "caption",
+                        "children": [
+                          {
+                            "tag": "svg",
+                            "ns": "http://www.w3.org/2000/svg",
+                            "children": [
+                              {
+                                "text": "foo"
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  },
+                  {
+                    "text": "bar"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><table><caption><svg>foo</svg></caption></table>bar</body></html>",
+        "noQuirksBodyHtml": "<table><caption><svg>foo</svg></caption></table>bar"
+      }
+    },
+    {
+      "data": "<table><tr><td><svg><desc><td></desc><circle>",
+      "errors": [
+        "(1,7): expected-doctype-but-got-start-tag",
+        "(1,30): unexpected-cell-end-tag",
+        "(1,37): unexpected-end-tag",
+        "(1,45): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true,
+            "tbody": true,
+            "tr": true,
+            "td": true,
+            "svg svg": true,
+            "svg desc": true,
+            "circle": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "tbody",
+                        "children": [
+                          {
+                            "tag": "tr",
+                            "children": [
+                              {
+                                "tag": "td",
+                                "children": [
+                                  {
+                                    "tag": "svg",
+                                    "ns": "http://www.w3.org/2000/svg",
+                                    "children": [
+                                      {
+                                        "tag": "desc",
+                                        "ns": "http://www.w3.org/2000/svg"
+                                      }
+                                    ]
+                                  }
+                                ]
+                              },
+                              {
+                                "tag": "td",
+                                "children": [
+                                  {
+                                    "tag": "circle"
+                                  }
+                                ]
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><table><tbody><tr><td><svg><desc></desc></svg></td><td><circle></circle></td></tr></tbody></table></body></html>",
+        "noQuirksBodyHtml": "<table><tbody><tr><td><svg><desc></desc></svg></td><td><circle></circle></td></tr></tbody></table>"
+      }
+    }
+  ],
+  "plain-text-unsafe.dat": [
+    {
+      "data": "FOO&#x000D;ZOO",
+      "errors": [
+        "(1,3): expected-doctype-but-got-chars",
+        "(1,11): illegal-codepoint-for-numeric-entity"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "FOO\rZOO"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>FOO\rZOO</body></html>",
+        "noQuirksBodyHtml": "FOO\rZOO"
+      }
+    },
+    {
+      "data": "<html>\u0000<frameset></frameset>",
+      "errors": [
+        "(1,6): expected-doctype-but-got-start-tag",
+        "(1,7): invalid-codepoint",
+        "(1,7): invalid-codepoint-in-body",
+        "(1,17): unexpected-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "frameset": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "frameset"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><frameset></frameset></html>",
+        "noQuirksBodyHtml": ""
+      }
+    },
+    {
+      "data": "<html> \u0000 <frameset></frameset>",
+      "errors": [
+        "(1,6): expected-doctype-but-got-start-tag",
+        "(1,8): invalid-codepoint",
+        "(1,8): invalid-codepoint-in-body",
+        "(1,19): unexpected-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "frameset": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "frameset"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><frameset></frameset></html>",
+        "noQuirksBodyHtml": "  "
+      }
+    },
+    {
+      "data": "<html>a\u0000a<frameset></frameset>",
+      "errors": [
+        "(1,6): expected-doctype-but-got-start-tag",
+        "(1,8): invalid-codepoint",
+        "(1,8): invalid-codepoint-in-body",
+        "(1,19): unexpected-start-tag",
+        "(1,30): unexpected-end-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "aa"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>aa</body></html>",
+        "noQuirksBodyHtml": "aa"
+      }
+    },
+    {
+      "data": "<html>\u0000\u0000<frameset></frameset>",
+      "errors": [
+        "(1,6): expected-doctype-but-got-start-tag",
+        "(1,7): invalid-codepoint",
+        "(1,7): invalid-codepoint-in-body",
+        "(1,8): invalid-codepoint",
+        "(1,8): invalid-codepoint-in-body",
+        "(1,18): unexpected-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "frameset": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "frameset"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><frameset></frameset></html>",
+        "noQuirksBodyHtml": ""
+      }
+    },
+    {
+      "data": "<html>\u0000\n <frameset></frameset>",
+      "errors": [
+        "(1,6): expected-doctype-but-got-start-tag",
+        "(1,7): invalid-codepoint",
+        "(1,7): invalid-codepoint-in-body",
+        "(2,11): unexpected-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "frameset": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "frameset"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><frameset></frameset></html>",
+        "noQuirksBodyHtml": "\n "
+      }
+    },
+    {
+      "data": "<html><select>\u0000",
+      "errors": [
+        "(1,6): expected-doctype-but-got-start-tag",
+        "(1,15): invalid-codepoint",
+        "(1,15): invalid-codepoint-in-select",
+        "(1,15): eof-in-select"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "select": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "select"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><select></select></body></html>",
+        "noQuirksBodyHtml": "<select></select>"
+      }
+    },
+    {
+      "data": "\u0000",
+      "errors": [
+        "(1,1): invalid-codepoint",
+        "(1,1): expected-doctype-but-got-chars",
+        "(1,1): invalid-codepoint-in-body"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body></body></html>",
+        "noQuirksBodyHtml": ""
+      }
+    },
+    {
+      "data": "<body>\u0000",
+      "errors": [
+        "(1,6): expected-doctype-but-got-start-tag",
+        "(1,7): invalid-codepoint",
+        "(1,7): invalid-codepoint-in-body"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body></body></html>",
+        "noQuirksBodyHtml": ""
+      }
+    },
+    {
+      "data": "<plaintext>\u0000filler\u0000text\u0000",
+      "errors": [
+        "(1,11): expected-doctype-but-got-start-tag",
+        "(1,12): invalid-codepoint",
+        "(1,19): invalid-codepoint",
+        "(1,24): invalid-codepoint",
+        "(1,24): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "plaintext": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "plaintext",
+                    "children": [
+                      {
+                        "text": "�filler�text�",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><plaintext>�filler�text�</plaintext></body></html>",
+        "noQuirksBodyHtml": "<plaintext>�filler�text�</plaintext>"
+      }
+    },
+    {
+      "data": "<svg><![CDATA[\u0000filler\u0000text\u0000]]>",
+      "errors": [
+        "(1,5): expected-doctype-but-got-start-tag",
+        "(1,30): invalid-codepoint",
+        "(1,30): invalid-codepoint",
+        "(1,30): invalid-codepoint",
+        "(1,30): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "svg svg": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "svg",
+                    "ns": "http://www.w3.org/2000/svg",
+                    "children": [
+                      {
+                        "text": "�filler�text�"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><svg>�filler�text�</svg></body></html>",
+        "noQuirksBodyHtml": "<svg>�filler�text�</svg>"
+      }
+    },
+    {
+      "data": "<body><!\u0000>",
+      "errors": [
+        "(1,6): expected-doctype-but-got-start-tag",
+        "(1,8): expected-dashes-or-doctype"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "comment": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "comment": "�"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><!--�--></body></html>",
+        "noQuirksBodyHtml": "<!--�-->"
+      }
+    },
+    {
+      "data": "<body><!\u0000filler\u0000text>",
+      "errors": [
+        "(1,6): expected-doctype-but-got-start-tag",
+        "(1,8): expected-dashes-or-doctype"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "comment": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "comment": "�filler�text"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><!--�filler�text--></body></html>",
+        "noQuirksBodyHtml": "<!--�filler�text-->"
+      }
+    },
+    {
+      "data": "<body><svg><foreignObject>\u0000filler\u0000text",
+      "errors": [
+        "(1,6): expected-doctype-but-got-start-tag",
+        "(1,27): invalid-codepoint",
+        "(1,27): invalid-codepoint-in-body",
+        "(1,34): invalid-codepoint",
+        "(1,34): invalid-codepoint-in-body",
+        "(1,38): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "svg svg": true,
+            "svg foreignObject": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "svg",
+                    "ns": "http://www.w3.org/2000/svg",
+                    "children": [
+                      {
+                        "tag": "foreignObject",
+                        "ns": "http://www.w3.org/2000/svg",
+                        "children": [
+                          {
+                            "text": "fillertext"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><svg><foreignObject>fillertext</foreignObject></svg></body></html>",
+        "noQuirksBodyHtml": "<svg><foreignObject>fillertext</foreignObject></svg>"
+      }
+    },
+    {
+      "data": "<svg>\u0000filler\u0000text",
+      "errors": [
+        "(1,5): expected-doctype-but-got-start-tag",
+        "(1,6): invalid-codepoint",
+        "(1,6): invalid-codepoint-in-foreign-content",
+        "(1,13): invalid-codepoint",
+        "(1,13): invalid-codepoint-in-foreign-content",
+        "(1,17): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "svg svg": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "svg",
+                    "ns": "http://www.w3.org/2000/svg",
+                    "children": [
+                      {
+                        "text": "�filler�text"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><svg>�filler�text</svg></body></html>",
+        "noQuirksBodyHtml": "<svg>�filler�text</svg>"
+      }
+    },
+    {
+      "data": "<svg>\u0000<frameset>",
+      "errors": [
+        "(1,5): expected-doctype-but-got-start-tag",
+        "(1,6): invalid-codepoint",
+        "(1,6): invalid-codepoint-in-foreign-content",
+        "(1,16): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "svg svg": true,
+            "svg frameset": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "svg",
+                    "ns": "http://www.w3.org/2000/svg",
+                    "children": [
+                      {
+                        "text": "�"
+                      },
+                      {
+                        "tag": "frameset",
+                        "ns": "http://www.w3.org/2000/svg"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><svg>�<frameset></frameset></svg></body></html>",
+        "noQuirksBodyHtml": "<svg>�<frameset></frameset></svg>"
+      }
+    },
+    {
+      "data": "<svg>\u0000 <frameset>",
+      "errors": [
+        "(1,5): expected-doctype-but-got-start-tag",
+        "(1,6): invalid-codepoint",
+        "(1,6): invalid-codepoint-in-foreign-content",
+        "(1,17): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "svg svg": true,
+            "svg frameset": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "svg",
+                    "ns": "http://www.w3.org/2000/svg",
+                    "children": [
+                      {
+                        "text": "� "
+                      },
+                      {
+                        "tag": "frameset",
+                        "ns": "http://www.w3.org/2000/svg"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><svg>� <frameset></frameset></svg></body></html>",
+        "noQuirksBodyHtml": "<svg>� <frameset></frameset></svg>"
+      }
+    },
+    {
+      "data": "<svg>\u0000a<frameset>",
+      "errors": [
+        "(1,5): expected-doctype-but-got-start-tag",
+        "(1,6): invalid-codepoint",
+        "(1,6): invalid-codepoint-in-foreign-content",
+        "(1,17): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "svg svg": true,
+            "svg frameset": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "svg",
+                    "ns": "http://www.w3.org/2000/svg",
+                    "children": [
+                      {
+                        "text": "�a"
+                      },
+                      {
+                        "tag": "frameset",
+                        "ns": "http://www.w3.org/2000/svg"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><svg>�a<frameset></frameset></svg></body></html>",
+        "noQuirksBodyHtml": "<svg>�a<frameset></frameset></svg>"
+      }
+    },
+    {
+      "data": "<svg>\u0000</svg><frameset>",
+      "errors": [
+        "(1,5): expected-doctype-but-got-start-tag",
+        "(1,6): invalid-codepoint",
+        "(1,6): invalid-codepoint-in-foreign-content",
+        "(1,22): unexpected-start-tag",
+        "(1,22): eof-in-frameset"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "frameset": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "frameset"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><frameset></frameset></html>",
+        "noQuirksBodyHtml": "<svg>�</svg>"
+      }
+    },
+    {
+      "data": "<svg>\u0000 </svg><frameset>",
+      "errors": [
+        "(1,5): expected-doctype-but-got-start-tag",
+        "(1,6): invalid-codepoint",
+        "(1,6): invalid-codepoint-in-foreign-content",
+        "(1,23): unexpected-start-tag",
+        "(1,23): eof-in-frameset"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "frameset": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "frameset"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><frameset></frameset></html>",
+        "noQuirksBodyHtml": "<svg>� </svg>"
+      }
+    },
+    {
+      "data": "<svg>\u0000a</svg><frameset>",
+      "errors": [
+        "(1,5): expected-doctype-but-got-start-tag",
+        "(1,6): invalid-codepoint",
+        "(1,6): invalid-codepoint-in-foreign-content",
+        "(1,23): unexpected-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "svg svg": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "svg",
+                    "ns": "http://www.w3.org/2000/svg",
+                    "children": [
+                      {
+                        "text": "�a"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><svg>�a</svg></body></html>",
+        "noQuirksBodyHtml": "<svg>�a</svg>"
+      }
+    },
+    {
+      "data": "<svg><path></path></svg><frameset>",
+      "errors": [
+        "(1,5): expected-doctype-but-got-start-tag",
+        "(1,34): unexpected-start-tag",
+        "(1,34): eof-in-frameset"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "frameset": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "frameset"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><frameset></frameset></html>",
+        "noQuirksBodyHtml": "<svg><path></path></svg>"
+      }
+    },
+    {
+      "data": "<svg><p><frameset>",
+      "errors": [
+        "(1, 5) expected-doctype-but-got-start-tag",
+        "(1, 8) unexpected-html-element-in-foreign-content",
+        "(1, 18) unexpected-start-tag",
+        "(1, 18) eof-in-frameset"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "frameset": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "frameset"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><frameset></frameset></html>",
+        "noQuirksBodyHtml": "<svg><p><frameset></frameset></p></svg>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><pre>\r\n\r\nA</pre>",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "pre": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "pre",
+                    "children": [
+                      {
+                        "text": "\nA"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><pre>\nA</pre></body></html>",
+        "noQuirksBodyHtml": "<pre>\nA</pre>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><pre>\r\rA</pre>",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "pre": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "pre",
+                    "children": [
+                      {
+                        "text": "\nA"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><pre>\nA</pre></body></html>",
+        "noQuirksBodyHtml": "<pre>\nA</pre>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><pre>\rA</pre>",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "pre": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "pre",
+                    "children": [
+                      {
+                        "text": "A"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><pre>A</pre></body></html>",
+        "noQuirksBodyHtml": "<pre>A</pre>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><table><tr><td><math><mtext>\u0000a",
+      "errors": [
+        "(1,44): invalid-codepoint",
+        "(1,44): invalid-codepoint-in-body",
+        "(1,45): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true,
+            "tbody": true,
+            "tr": true,
+            "td": true,
+            "math math": true,
+            "math mtext": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "tbody",
+                        "children": [
+                          {
+                            "tag": "tr",
+                            "children": [
+                              {
+                                "tag": "td",
+                                "children": [
+                                  {
+                                    "tag": "math",
+                                    "ns": "http://www.w3.org/1998/Math/MathML",
+                                    "children": [
+                                      {
+                                        "tag": "mtext",
+                                        "ns": "http://www.w3.org/1998/Math/MathML",
+                                        "children": [
+                                          {
+                                            "text": "a"
+                                          }
+                                        ]
+                                      }
+                                    ]
+                                  }
+                                ]
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><table><tbody><tr><td><math><mtext>a</mtext></math></td></tr></tbody></table></body></html>",
+        "noQuirksBodyHtml": "<table><tbody><tr><td><math><mtext>a</mtext></math></td></tr></tbody></table>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><table><tr><td><svg><foreignObject>\u0000a",
+      "errors": [
+        "(1,51): invalid-codepoint",
+        "(1,51): invalid-codepoint-in-body",
+        "(1,52): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true,
+            "tbody": true,
+            "tr": true,
+            "td": true,
+            "svg svg": true,
+            "svg foreignObject": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "tbody",
+                        "children": [
+                          {
+                            "tag": "tr",
+                            "children": [
+                              {
+                                "tag": "td",
+                                "children": [
+                                  {
+                                    "tag": "svg",
+                                    "ns": "http://www.w3.org/2000/svg",
+                                    "children": [
+                                      {
+                                        "tag": "foreignObject",
+                                        "ns": "http://www.w3.org/2000/svg",
+                                        "children": [
+                                          {
+                                            "text": "a"
+                                          }
+                                        ]
+                                      }
+                                    ]
+                                  }
+                                ]
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><table><tbody><tr><td><svg><foreignObject>a</foreignObject></svg></td></tr></tbody></table></body></html>",
+        "noQuirksBodyHtml": "<table><tbody><tr><td><svg><foreignObject>a</foreignObject></svg></td></tr></tbody></table>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><math><mi>a\u0000b",
+      "errors": [
+        "(1,27): invalid-codepoint",
+        "(1,27): invalid-codepoint-in-body",
+        "(1,28): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "math math": true,
+            "math mi": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "math",
+                    "ns": "http://www.w3.org/1998/Math/MathML",
+                    "children": [
+                      {
+                        "tag": "mi",
+                        "ns": "http://www.w3.org/1998/Math/MathML",
+                        "children": [
+                          {
+                            "text": "ab"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><math><mi>ab</mi></math></body></html>",
+        "noQuirksBodyHtml": "<math><mi>ab</mi></math>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><math><mo>a\u0000b",
+      "errors": [
+        "(1,27): invalid-codepoint",
+        "(1,27): invalid-codepoint-in-body",
+        "(1,28): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "math math": true,
+            "math mo": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "math",
+                    "ns": "http://www.w3.org/1998/Math/MathML",
+                    "children": [
+                      {
+                        "tag": "mo",
+                        "ns": "http://www.w3.org/1998/Math/MathML",
+                        "children": [
+                          {
+                            "text": "ab"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><math><mo>ab</mo></math></body></html>",
+        "noQuirksBodyHtml": "<math><mo>ab</mo></math>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><math><mn>a\u0000b",
+      "errors": [
+        "(1,27): invalid-codepoint",
+        "(1,27): invalid-codepoint-in-body",
+        "(1,28): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "math math": true,
+            "math mn": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "math",
+                    "ns": "http://www.w3.org/1998/Math/MathML",
+                    "children": [
+                      {
+                        "tag": "mn",
+                        "ns": "http://www.w3.org/1998/Math/MathML",
+                        "children": [
+                          {
+                            "text": "ab"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><math><mn>ab</mn></math></body></html>",
+        "noQuirksBodyHtml": "<math><mn>ab</mn></math>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><math><ms>a\u0000b",
+      "errors": [
+        "(1,27): invalid-codepoint",
+        "(1,27): invalid-codepoint-in-body",
+        "(1,28): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "math math": true,
+            "math ms": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "math",
+                    "ns": "http://www.w3.org/1998/Math/MathML",
+                    "children": [
+                      {
+                        "tag": "ms",
+                        "ns": "http://www.w3.org/1998/Math/MathML",
+                        "children": [
+                          {
+                            "text": "ab"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><math><ms>ab</ms></math></body></html>",
+        "noQuirksBodyHtml": "<math><ms>ab</ms></math>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><math><mtext>a\u0000b",
+      "errors": [
+        "(1,30): invalid-codepoint",
+        "(1,30): invalid-codepoint-in-body",
+        "(1,31): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "math math": true,
+            "math mtext": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "math",
+                    "ns": "http://www.w3.org/1998/Math/MathML",
+                    "children": [
+                      {
+                        "tag": "mtext",
+                        "ns": "http://www.w3.org/1998/Math/MathML",
+                        "children": [
+                          {
+                            "text": "ab"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><math><mtext>ab</mtext></math></body></html>",
+        "noQuirksBodyHtml": "<math><mtext>ab</mtext></math>"
+      }
+    }
+  ],
+  "ruby.dat": [
+    {
+      "data": "<html><ruby>a<rb>b<rb></ruby></html>",
+      "errors": [
+        "(1,6): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "ruby": true,
+            "rb": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "ruby",
+                    "children": [
+                      {
+                        "text": "a"
+                      },
+                      {
+                        "tag": "rb",
+                        "children": [
+                          {
+                            "text": "b"
+                          }
+                        ]
+                      },
+                      {
+                        "tag": "rb"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><ruby>a<rb>b</rb><rb></rb></ruby></body></html>",
+        "noQuirksBodyHtml": "<ruby>a<rb>b</rb><rb></rb></ruby>"
+      }
+    },
+    {
+      "data": "<html><ruby>a<rb>b<rt></ruby></html>",
+      "errors": [
+        "(1,6): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "ruby": true,
+            "rb": true,
+            "rt": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "ruby",
+                    "children": [
+                      {
+                        "text": "a"
+                      },
+                      {
+                        "tag": "rb",
+                        "children": [
+                          {
+                            "text": "b"
+                          }
+                        ]
+                      },
+                      {
+                        "tag": "rt"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><ruby>a<rb>b</rb><rt></rt></ruby></body></html>",
+        "noQuirksBodyHtml": "<ruby>a<rb>b</rb><rt></rt></ruby>"
+      }
+    },
+    {
+      "data": "<html><ruby>a<rb>b<rtc></ruby></html>",
+      "errors": [
+        "(1,6): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "ruby": true,
+            "rb": true,
+            "rtc": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "ruby",
+                    "children": [
+                      {
+                        "text": "a"
+                      },
+                      {
+                        "tag": "rb",
+                        "children": [
+                          {
+                            "text": "b"
+                          }
+                        ]
+                      },
+                      {
+                        "tag": "rtc"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><ruby>a<rb>b</rb><rtc></rtc></ruby></body></html>",
+        "noQuirksBodyHtml": "<ruby>a<rb>b</rb><rtc></rtc></ruby>"
+      }
+    },
+    {
+      "data": "<html><ruby>a<rb>b<rp></ruby></html>",
+      "errors": [
+        "(1,6): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "ruby": true,
+            "rb": true,
+            "rp": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "ruby",
+                    "children": [
+                      {
+                        "text": "a"
+                      },
+                      {
+                        "tag": "rb",
+                        "children": [
+                          {
+                            "text": "b"
+                          }
+                        ]
+                      },
+                      {
+                        "tag": "rp"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><ruby>a<rb>b</rb><rp></rp></ruby></body></html>",
+        "noQuirksBodyHtml": "<ruby>a<rb>b</rb><rp></rp></ruby>"
+      }
+    },
+    {
+      "data": "<html><ruby>a<rb>b<span></ruby></html>",
+      "errors": [
+        "(1,6): expected-doctype-but-got-start-tag",
+        "(1,31): unexpected-end-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "ruby": true,
+            "rb": true,
+            "span": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "ruby",
+                    "children": [
+                      {
+                        "text": "a"
+                      },
+                      {
+                        "tag": "rb",
+                        "children": [
+                          {
+                            "text": "b"
+                          },
+                          {
+                            "tag": "span"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><ruby>a<rb>b<span></span></rb></ruby></body></html>",
+        "noQuirksBodyHtml": "<ruby>a<rb>b<span></span></rb></ruby>"
+      }
+    },
+    {
+      "data": "<html><ruby>a<rt>b<rb></ruby></html>",
+      "errors": [
+        "(1,6): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "ruby": true,
+            "rt": true,
+            "rb": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "ruby",
+                    "children": [
+                      {
+                        "text": "a"
+                      },
+                      {
+                        "tag": "rt",
+                        "children": [
+                          {
+                            "text": "b"
+                          }
+                        ]
+                      },
+                      {
+                        "tag": "rb"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><ruby>a<rt>b</rt><rb></rb></ruby></body></html>",
+        "noQuirksBodyHtml": "<ruby>a<rt>b</rt><rb></rb></ruby>"
+      }
+    },
+    {
+      "data": "<html><ruby>a<rt>b<rt></ruby></html>",
+      "errors": [
+        "(1,6): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "ruby": true,
+            "rt": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "ruby",
+                    "children": [
+                      {
+                        "text": "a"
+                      },
+                      {
+                        "tag": "rt",
+                        "children": [
+                          {
+                            "text": "b"
+                          }
+                        ]
+                      },
+                      {
+                        "tag": "rt"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><ruby>a<rt>b</rt><rt></rt></ruby></body></html>",
+        "noQuirksBodyHtml": "<ruby>a<rt>b</rt><rt></rt></ruby>"
+      }
+    },
+    {
+      "data": "<html><ruby>a<rt>b<rtc></ruby></html>",
+      "errors": [
+        "(1,6): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "ruby": true,
+            "rt": true,
+            "rtc": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "ruby",
+                    "children": [
+                      {
+                        "text": "a"
+                      },
+                      {
+                        "tag": "rt",
+                        "children": [
+                          {
+                            "text": "b"
+                          }
+                        ]
+                      },
+                      {
+                        "tag": "rtc"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><ruby>a<rt>b</rt><rtc></rtc></ruby></body></html>",
+        "noQuirksBodyHtml": "<ruby>a<rt>b</rt><rtc></rtc></ruby>"
+      }
+    },
+    {
+      "data": "<html><ruby>a<rt>b<rp></ruby></html>",
+      "errors": [
+        "(1,6): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "ruby": true,
+            "rt": true,
+            "rp": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "ruby",
+                    "children": [
+                      {
+                        "text": "a"
+                      },
+                      {
+                        "tag": "rt",
+                        "children": [
+                          {
+                            "text": "b"
+                          }
+                        ]
+                      },
+                      {
+                        "tag": "rp"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><ruby>a<rt>b</rt><rp></rp></ruby></body></html>",
+        "noQuirksBodyHtml": "<ruby>a<rt>b</rt><rp></rp></ruby>"
+      }
+    },
+    {
+      "data": "<html><ruby>a<rt>b<span></ruby></html>",
+      "errors": [
+        "(1,6): expected-doctype-but-got-start-tag",
+        "(1,31): unexpected-end-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "ruby": true,
+            "rt": true,
+            "span": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "ruby",
+                    "children": [
+                      {
+                        "text": "a"
+                      },
+                      {
+                        "tag": "rt",
+                        "children": [
+                          {
+                            "text": "b"
+                          },
+                          {
+                            "tag": "span"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><ruby>a<rt>b<span></span></rt></ruby></body></html>",
+        "noQuirksBodyHtml": "<ruby>a<rt>b<span></span></rt></ruby>"
+      }
+    },
+    {
+      "data": "<html><ruby>a<rtc>b<rb></ruby></html>",
+      "errors": [
+        "(1,6): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "ruby": true,
+            "rtc": true,
+            "rb": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "ruby",
+                    "children": [
+                      {
+                        "text": "a"
+                      },
+                      {
+                        "tag": "rtc",
+                        "children": [
+                          {
+                            "text": "b"
+                          }
+                        ]
+                      },
+                      {
+                        "tag": "rb"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><ruby>a<rtc>b</rtc><rb></rb></ruby></body></html>",
+        "noQuirksBodyHtml": "<ruby>a<rtc>b</rtc><rb></rb></ruby>"
+      }
+    },
+    {
+      "data": "<html><ruby>a<rtc>b<rt>c<rt>d</ruby></html>",
+      "errors": [
+        "(1,6): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "ruby": true,
+            "rtc": true,
+            "rt": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "ruby",
+                    "children": [
+                      {
+                        "text": "a"
+                      },
+                      {
+                        "tag": "rtc",
+                        "children": [
+                          {
+                            "text": "b"
+                          },
+                          {
+                            "tag": "rt",
+                            "children": [
+                              {
+                                "text": "c"
+                              }
+                            ]
+                          },
+                          {
+                            "tag": "rt",
+                            "children": [
+                              {
+                                "text": "d"
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><ruby>a<rtc>b<rt>c</rt><rt>d</rt></rtc></ruby></body></html>",
+        "noQuirksBodyHtml": "<ruby>a<rtc>b<rt>c</rt><rt>d</rt></rtc></ruby>"
+      }
+    },
+    {
+      "data": "<html><ruby>a<rtc>b<rtc></ruby></html>",
+      "errors": [
+        "(1,6): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "ruby": true,
+            "rtc": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "ruby",
+                    "children": [
+                      {
+                        "text": "a"
+                      },
+                      {
+                        "tag": "rtc",
+                        "children": [
+                          {
+                            "text": "b"
+                          }
+                        ]
+                      },
+                      {
+                        "tag": "rtc"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><ruby>a<rtc>b</rtc><rtc></rtc></ruby></body></html>",
+        "noQuirksBodyHtml": "<ruby>a<rtc>b</rtc><rtc></rtc></ruby>"
+      }
+    },
+    {
+      "data": "<html><ruby>a<rtc>b<rp></ruby></html>",
+      "errors": [
+        "(1,6): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "ruby": true,
+            "rtc": true,
+            "rp": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "ruby",
+                    "children": [
+                      {
+                        "text": "a"
+                      },
+                      {
+                        "tag": "rtc",
+                        "children": [
+                          {
+                            "text": "b"
+                          },
+                          {
+                            "tag": "rp"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><ruby>a<rtc>b<rp></rp></rtc></ruby></body></html>",
+        "noQuirksBodyHtml": "<ruby>a<rtc>b<rp></rp></rtc></ruby>"
+      }
+    },
+    {
+      "data": "<html><ruby>a<rtc>b<span></ruby></html>",
+      "errors": [
+        "(1,6): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "ruby": true,
+            "rtc": true,
+            "span": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "ruby",
+                    "children": [
+                      {
+                        "text": "a"
+                      },
+                      {
+                        "tag": "rtc",
+                        "children": [
+                          {
+                            "text": "b"
+                          },
+                          {
+                            "tag": "span"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><ruby>a<rtc>b<span></span></rtc></ruby></body></html>",
+        "noQuirksBodyHtml": "<ruby>a<rtc>b<span></span></rtc></ruby>"
+      }
+    },
+    {
+      "data": "<html><ruby>a<rp>b<rb></ruby></html>",
+      "errors": [
+        "(1,6): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "ruby": true,
+            "rp": true,
+            "rb": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "ruby",
+                    "children": [
+                      {
+                        "text": "a"
+                      },
+                      {
+                        "tag": "rp",
+                        "children": [
+                          {
+                            "text": "b"
+                          }
+                        ]
+                      },
+                      {
+                        "tag": "rb"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><ruby>a<rp>b</rp><rb></rb></ruby></body></html>",
+        "noQuirksBodyHtml": "<ruby>a<rp>b</rp><rb></rb></ruby>"
+      }
+    },
+    {
+      "data": "<html><ruby>a<rp>b<rt></ruby></html>",
+      "errors": [
+        "(1,6): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "ruby": true,
+            "rp": true,
+            "rt": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "ruby",
+                    "children": [
+                      {
+                        "text": "a"
+                      },
+                      {
+                        "tag": "rp",
+                        "children": [
+                          {
+                            "text": "b"
+                          }
+                        ]
+                      },
+                      {
+                        "tag": "rt"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><ruby>a<rp>b</rp><rt></rt></ruby></body></html>",
+        "noQuirksBodyHtml": "<ruby>a<rp>b</rp><rt></rt></ruby>"
+      }
+    },
+    {
+      "data": "<html><ruby>a<rp>b<rtc></ruby></html>",
+      "errors": [
+        "(1,6): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "ruby": true,
+            "rp": true,
+            "rtc": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "ruby",
+                    "children": [
+                      {
+                        "text": "a"
+                      },
+                      {
+                        "tag": "rp",
+                        "children": [
+                          {
+                            "text": "b"
+                          }
+                        ]
+                      },
+                      {
+                        "tag": "rtc"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><ruby>a<rp>b</rp><rtc></rtc></ruby></body></html>",
+        "noQuirksBodyHtml": "<ruby>a<rp>b</rp><rtc></rtc></ruby>"
+      }
+    },
+    {
+      "data": "<html><ruby>a<rp>b<rp></ruby></html>",
+      "errors": [
+        "(1,6): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "ruby": true,
+            "rp": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "ruby",
+                    "children": [
+                      {
+                        "text": "a"
+                      },
+                      {
+                        "tag": "rp",
+                        "children": [
+                          {
+                            "text": "b"
+                          }
+                        ]
+                      },
+                      {
+                        "tag": "rp"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><ruby>a<rp>b</rp><rp></rp></ruby></body></html>",
+        "noQuirksBodyHtml": "<ruby>a<rp>b</rp><rp></rp></ruby>"
+      }
+    },
+    {
+      "data": "<html><ruby>a<rp>b<span></ruby></html>",
+      "errors": [
+        "(1,6): expected-doctype-but-got-start-tag",
+        "(1,31): unexpected-end-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "ruby": true,
+            "rp": true,
+            "span": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "ruby",
+                    "children": [
+                      {
+                        "text": "a"
+                      },
+                      {
+                        "tag": "rp",
+                        "children": [
+                          {
+                            "text": "b"
+                          },
+                          {
+                            "tag": "span"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><ruby>a<rp>b<span></span></rp></ruby></body></html>",
+        "noQuirksBodyHtml": "<ruby>a<rp>b<span></span></rp></ruby>"
+      }
+    },
+    {
+      "data": "<html><ruby><rtc><ruby>a<rb>b<rt></ruby></ruby></html>",
+      "errors": [
+        "(1,6): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "ruby": true,
+            "rtc": true,
+            "rb": true,
+            "rt": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "ruby",
+                    "children": [
+                      {
+                        "tag": "rtc",
+                        "children": [
+                          {
+                            "tag": "ruby",
+                            "children": [
+                              {
+                                "text": "a"
+                              },
+                              {
+                                "tag": "rb",
+                                "children": [
+                                  {
+                                    "text": "b"
+                                  }
+                                ]
+                              },
+                              {
+                                "tag": "rt"
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><ruby><rtc><ruby>a<rb>b</rb><rt></rt></ruby></rtc></ruby></body></html>",
+        "noQuirksBodyHtml": "<ruby><rtc><ruby>a<rb>b</rb><rt></rt></ruby></rtc></ruby>"
+      }
+    }
+  ],
+  "scriptdata01.dat": [
+    {
+      "data": "FOO<script>'Hello'</script>BAR",
+      "errors": [
+        "(1,3): expected-doctype-but-got-chars"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "script": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "FOO"
+                  },
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "'Hello'",
+                        "no_escape": true
+                      }
+                    ]
+                  },
+                  {
+                    "text": "BAR"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>FOO<script>'Hello'</script>BAR</body></html>",
+        "noQuirksBodyHtml": "FOO<script>'Hello'</script>BAR"
+      }
+    },
+    {
+      "data": "FOO<script></script>BAR",
+      "errors": [
+        "(1,3): expected-doctype-but-got-chars"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "script": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "FOO"
+                  },
+                  {
+                    "tag": "script"
+                  },
+                  {
+                    "text": "BAR"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>FOO<script></script>BAR</body></html>",
+        "noQuirksBodyHtml": "FOO<script></script>BAR"
+      }
+    },
+    {
+      "data": "FOO<script></script >BAR",
+      "errors": [
+        "(1,3): expected-doctype-but-got-chars"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "script": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "FOO"
+                  },
+                  {
+                    "tag": "script"
+                  },
+                  {
+                    "text": "BAR"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>FOO<script></script>BAR</body></html>",
+        "noQuirksBodyHtml": "FOO<script></script>BAR"
+      }
+    },
+    {
+      "data": "FOO<script></script/>BAR",
+      "errors": [
+        "(1,3): expected-doctype-but-got-chars",
+        "(1,21): self-closing-flag-on-end-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "script": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "FOO"
+                  },
+                  {
+                    "tag": "script"
+                  },
+                  {
+                    "text": "BAR"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>FOO<script></script>BAR</body></html>",
+        "noQuirksBodyHtml": "FOO<script></script>BAR"
+      }
+    },
+    {
+      "data": "FOO<script></script/ >BAR",
+      "errors": [
+        "(1,3): expected-doctype-but-got-chars",
+        "(1,20): unexpected-character-after-solidus-in-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "script": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "FOO"
+                  },
+                  {
+                    "tag": "script"
+                  },
+                  {
+                    "text": "BAR"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>FOO<script></script>BAR</body></html>",
+        "noQuirksBodyHtml": "FOO<script></script>BAR"
+      }
+    },
+    {
+      "data": "FOO<script type=\"text/plain\"></scriptx>BAR",
+      "errors": [
+        "(1,3): expected-doctype-but-got-chars",
+        "(1,42): expected-named-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "script": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "FOO"
+                  },
+                  {
+                    "tag": "script",
+                    "attrs": [
+                      {
+                        "name": "type",
+                        "value": "text/plain"
+                      }
+                    ],
+                    "children": [
+                      {
+                        "text": "</scriptx>BAR",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>FOO<script type=\"text/plain\"></scriptx>BAR</script></body></html>",
+        "noQuirksBodyHtml": "FOO<script type=\"text/plain\"></scriptx>BAR</script>"
+      }
+    },
+    {
+      "data": "FOO<script></script foo=\">\" dd>BAR",
+      "errors": [
+        "(1,3): expected-doctype-but-got-chars",
+        "(1,31): attributes-in-end-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "script": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "FOO"
+                  },
+                  {
+                    "tag": "script"
+                  },
+                  {
+                    "text": "BAR"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>FOO<script></script>BAR</body></html>",
+        "noQuirksBodyHtml": "FOO<script></script>BAR"
+      }
+    },
+    {
+      "data": "FOO<script>'<'</script>BAR",
+      "errors": [
+        "(1,3): expected-doctype-but-got-chars"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "script": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "FOO"
+                  },
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "'<'",
+                        "no_escape": true
+                      }
+                    ]
+                  },
+                  {
+                    "text": "BAR"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>FOO<script>'<'</script>BAR</body></html>",
+        "noQuirksBodyHtml": "FOO<script>'<'</script>BAR"
+      }
+    },
+    {
+      "data": "FOO<script>'<!'</script>BAR",
+      "errors": [
+        "(1,3): expected-doctype-but-got-chars"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "script": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "FOO"
+                  },
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "'<!'",
+                        "no_escape": true
+                      }
+                    ]
+                  },
+                  {
+                    "text": "BAR"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>FOO<script>'<!'</script>BAR</body></html>",
+        "noQuirksBodyHtml": "FOO<script>'<!'</script>BAR"
+      }
+    },
+    {
+      "data": "FOO<script>'<!-'</script>BAR",
+      "errors": [
+        "(1,3): expected-doctype-but-got-chars"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "script": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "FOO"
+                  },
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "'<!-'",
+                        "no_escape": true
+                      }
+                    ]
+                  },
+                  {
+                    "text": "BAR"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>FOO<script>'<!-'</script>BAR</body></html>",
+        "noQuirksBodyHtml": "FOO<script>'<!-'</script>BAR"
+      }
+    },
+    {
+      "data": "FOO<script>'<!--'</script>BAR",
+      "errors": [
+        "(1,3): expected-doctype-but-got-chars"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "script": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "FOO"
+                  },
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "'<!--'",
+                        "no_escape": true
+                      }
+                    ]
+                  },
+                  {
+                    "text": "BAR"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>FOO<script>'<!--'</script>BAR</body></html>",
+        "noQuirksBodyHtml": "FOO<script>'<!--'</script>BAR"
+      }
+    },
+    {
+      "data": "FOO<script>'<!---'</script>BAR",
+      "errors": [
+        "(1,3): expected-doctype-but-got-chars"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "script": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "FOO"
+                  },
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "'<!---'",
+                        "no_escape": true
+                      }
+                    ]
+                  },
+                  {
+                    "text": "BAR"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>FOO<script>'<!---'</script>BAR</body></html>",
+        "noQuirksBodyHtml": "FOO<script>'<!---'</script>BAR"
+      }
+    },
+    {
+      "data": "FOO<script>'<!-->'</script>BAR",
+      "errors": [
+        "(1,3): expected-doctype-but-got-chars"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "script": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "FOO"
+                  },
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "'<!-->'",
+                        "no_escape": true
+                      }
+                    ]
+                  },
+                  {
+                    "text": "BAR"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>FOO<script>'<!-->'</script>BAR</body></html>",
+        "noQuirksBodyHtml": "FOO<script>'<!-->'</script>BAR"
+      }
+    },
+    {
+      "data": "FOO<script>'<!-->'</script>BAR",
+      "errors": [
+        "(1,3): expected-doctype-but-got-chars"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "script": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "FOO"
+                  },
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "'<!-->'",
+                        "no_escape": true
+                      }
+                    ]
+                  },
+                  {
+                    "text": "BAR"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>FOO<script>'<!-->'</script>BAR</body></html>",
+        "noQuirksBodyHtml": "FOO<script>'<!-->'</script>BAR"
+      }
+    },
+    {
+      "data": "FOO<script>'<!-- potato'</script>BAR",
+      "errors": [
+        "(1,3): expected-doctype-but-got-chars"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "script": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "FOO"
+                  },
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "'<!-- potato'",
+                        "no_escape": true
+                      }
+                    ]
+                  },
+                  {
+                    "text": "BAR"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>FOO<script>'<!-- potato'</script>BAR</body></html>",
+        "noQuirksBodyHtml": "FOO<script>'<!-- potato'</script>BAR"
+      }
+    },
+    {
+      "data": "FOO<script>'<!-- <sCrIpt'</script>BAR",
+      "errors": [
+        "(1,3): expected-doctype-but-got-chars"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "script": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "FOO"
+                  },
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "'<!-- <sCrIpt'",
+                        "no_escape": true
+                      }
+                    ]
+                  },
+                  {
+                    "text": "BAR"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>FOO<script>'<!-- <sCrIpt'</script>BAR</body></html>",
+        "noQuirksBodyHtml": "FOO<script>'<!-- <sCrIpt'</script>BAR"
+      }
+    },
+    {
+      "data": "FOO<script type=\"text/plain\">'<!-- <sCrIpt>'</script>BAR",
+      "errors": [
+        "(1,3): expected-doctype-but-got-chars",
+        "(1,56): expected-script-data-but-got-eof",
+        "(1,56): expected-named-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "script": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "FOO"
+                  },
+                  {
+                    "tag": "script",
+                    "attrs": [
+                      {
+                        "name": "type",
+                        "value": "text/plain"
+                      }
+                    ],
+                    "children": [
+                      {
+                        "text": "'<!-- <sCrIpt>'</script>BAR",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>FOO<script type=\"text/plain\">'<!-- <sCrIpt>'</script>BAR</script></body></html>",
+        "noQuirksBodyHtml": "FOO<script type=\"text/plain\">'<!-- <sCrIpt>'</script>BAR</script>"
+      }
+    },
+    {
+      "data": "FOO<script type=\"text/plain\">'<!-- <sCrIpt> -'</script>BAR",
+      "errors": [
+        "(1,3): expected-doctype-but-got-chars",
+        "(1,58): expected-script-data-but-got-eof",
+        "(1,58): expected-named-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "script": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "FOO"
+                  },
+                  {
+                    "tag": "script",
+                    "attrs": [
+                      {
+                        "name": "type",
+                        "value": "text/plain"
+                      }
+                    ],
+                    "children": [
+                      {
+                        "text": "'<!-- <sCrIpt> -'</script>BAR",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>FOO<script type=\"text/plain\">'<!-- <sCrIpt> -'</script>BAR</script></body></html>",
+        "noQuirksBodyHtml": "FOO<script type=\"text/plain\">'<!-- <sCrIpt> -'</script>BAR</script>"
+      }
+    },
+    {
+      "data": "FOO<script type=\"text/plain\">'<!-- <sCrIpt> --'</script>BAR",
+      "errors": [
+        "(1,3): expected-doctype-but-got-chars",
+        "(1,59): expected-script-data-but-got-eof",
+        "(1,59): expected-named-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "script": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "FOO"
+                  },
+                  {
+                    "tag": "script",
+                    "attrs": [
+                      {
+                        "name": "type",
+                        "value": "text/plain"
+                      }
+                    ],
+                    "children": [
+                      {
+                        "text": "'<!-- <sCrIpt> --'</script>BAR",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>FOO<script type=\"text/plain\">'<!-- <sCrIpt> --'</script>BAR</script></body></html>",
+        "noQuirksBodyHtml": "FOO<script type=\"text/plain\">'<!-- <sCrIpt> --'</script>BAR</script>"
+      }
+    },
+    {
+      "data": "FOO<script>'<!-- <sCrIpt> -->'</script>BAR",
+      "errors": [
+        "(1,3): expected-doctype-but-got-chars"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "script": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "FOO"
+                  },
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "'<!-- <sCrIpt> -->'",
+                        "no_escape": true
+                      }
+                    ]
+                  },
+                  {
+                    "text": "BAR"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>FOO<script>'<!-- <sCrIpt> -->'</script>BAR</body></html>",
+        "noQuirksBodyHtml": "FOO<script>'<!-- <sCrIpt> -->'</script>BAR"
+      }
+    },
+    {
+      "data": "FOO<script type=\"text/plain\">'<!-- <sCrIpt> --!>'</script>BAR",
+      "errors": [
+        "(1,3): expected-doctype-but-got-chars",
+        "(1,61): expected-script-data-but-got-eof",
+        "(1,61): expected-named-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "script": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "FOO"
+                  },
+                  {
+                    "tag": "script",
+                    "attrs": [
+                      {
+                        "name": "type",
+                        "value": "text/plain"
+                      }
+                    ],
+                    "children": [
+                      {
+                        "text": "'<!-- <sCrIpt> --!>'</script>BAR",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>FOO<script type=\"text/plain\">'<!-- <sCrIpt> --!>'</script>BAR</script></body></html>",
+        "noQuirksBodyHtml": "FOO<script type=\"text/plain\">'<!-- <sCrIpt> --!>'</script>BAR</script>"
+      }
+    },
+    {
+      "data": "FOO<script type=\"text/plain\">'<!-- <sCrIpt> -- >'</script>BAR",
+      "errors": [
+        "(1,3): expected-doctype-but-got-chars",
+        "(1,61): expected-script-data-but-got-eof",
+        "(1,61): expected-named-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "script": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "FOO"
+                  },
+                  {
+                    "tag": "script",
+                    "attrs": [
+                      {
+                        "name": "type",
+                        "value": "text/plain"
+                      }
+                    ],
+                    "children": [
+                      {
+                        "text": "'<!-- <sCrIpt> -- >'</script>BAR",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>FOO<script type=\"text/plain\">'<!-- <sCrIpt> -- >'</script>BAR</script></body></html>",
+        "noQuirksBodyHtml": "FOO<script type=\"text/plain\">'<!-- <sCrIpt> -- >'</script>BAR</script>"
+      }
+    },
+    {
+      "data": "FOO<script type=\"text/plain\">'<!-- <sCrIpt '</script>BAR",
+      "errors": [
+        "(1,3): expected-doctype-but-got-chars",
+        "(1,56): expected-script-data-but-got-eof",
+        "(1,56): expected-named-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "script": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "FOO"
+                  },
+                  {
+                    "tag": "script",
+                    "attrs": [
+                      {
+                        "name": "type",
+                        "value": "text/plain"
+                      }
+                    ],
+                    "children": [
+                      {
+                        "text": "'<!-- <sCrIpt '</script>BAR",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>FOO<script type=\"text/plain\">'<!-- <sCrIpt '</script>BAR</script></body></html>",
+        "noQuirksBodyHtml": "FOO<script type=\"text/plain\">'<!-- <sCrIpt '</script>BAR</script>"
+      }
+    },
+    {
+      "data": "FOO<script type=\"text/plain\">'<!-- <sCrIpt/'</script>BAR",
+      "errors": [
+        "(1,3): expected-doctype-but-got-chars",
+        "(1,56): expected-script-data-but-got-eof",
+        "(1,56): expected-named-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "script": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "FOO"
+                  },
+                  {
+                    "tag": "script",
+                    "attrs": [
+                      {
+                        "name": "type",
+                        "value": "text/plain"
+                      }
+                    ],
+                    "children": [
+                      {
+                        "text": "'<!-- <sCrIpt/'</script>BAR",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>FOO<script type=\"text/plain\">'<!-- <sCrIpt/'</script>BAR</script></body></html>",
+        "noQuirksBodyHtml": "FOO<script type=\"text/plain\">'<!-- <sCrIpt/'</script>BAR</script>"
+      }
+    },
+    {
+      "data": "FOO<script type=\"text/plain\">'<!-- <sCrIpt\\'</script>BAR",
+      "errors": [
+        "(1,3): expected-doctype-but-got-chars"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "script": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "FOO"
+                  },
+                  {
+                    "tag": "script",
+                    "attrs": [
+                      {
+                        "name": "type",
+                        "value": "text/plain"
+                      }
+                    ],
+                    "children": [
+                      {
+                        "text": "'<!-- <sCrIpt\\'",
+                        "no_escape": true
+                      }
+                    ]
+                  },
+                  {
+                    "text": "BAR"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>FOO<script type=\"text/plain\">'<!-- <sCrIpt\\'</script>BAR</body></html>",
+        "noQuirksBodyHtml": "FOO<script type=\"text/plain\">'<!-- <sCrIpt\\'</script>BAR"
+      }
+    },
+    {
+      "data": "FOO<script type=\"text/plain\">'<!-- <sCrIpt/'</script>BAR</script>QUX",
+      "errors": [
+        "(1,3): expected-doctype-but-got-chars"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "script": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "FOO"
+                  },
+                  {
+                    "tag": "script",
+                    "attrs": [
+                      {
+                        "name": "type",
+                        "value": "text/plain"
+                      }
+                    ],
+                    "children": [
+                      {
+                        "text": "'<!-- <sCrIpt/'</script>BAR",
+                        "no_escape": true
+                      }
+                    ]
+                  },
+                  {
+                    "text": "QUX"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>FOO<script type=\"text/plain\">'<!-- <sCrIpt/'</script>BAR</script>QUX</body></html>",
+        "noQuirksBodyHtml": "FOO<script type=\"text/plain\">'<!-- <sCrIpt/'</script>BAR</script>QUX"
+      }
+    },
+    {
+      "data": "FOO<script><!--<script>-></script>--></script>QUX",
+      "errors": [
+        "(1,3): expected-doctype-but-got-chars"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "script": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "FOO"
+                  },
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "<!--<script>-></script>-->",
+                        "no_escape": true
+                      }
+                    ]
+                  },
+                  {
+                    "text": "QUX"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>FOO<script><!--<script>-></script>--></script>QUX</body></html>",
+        "noQuirksBodyHtml": "FOO<script><!--<script>-></script>--></script>QUX"
+      }
+    }
+  ],
+  "tables01.dat": [
+    {
+      "data": "<table><th>",
+      "errors": [
+        "(1,7): expected-doctype-but-got-start-tag",
+        "(1,11): unexpected-cell-in-table-body",
+        "(1,11): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true,
+            "tbody": true,
+            "tr": true,
+            "th": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "tbody",
+                        "children": [
+                          {
+                            "tag": "tr",
+                            "children": [
+                              {
+                                "tag": "th"
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><table><tbody><tr><th></th></tr></tbody></table></body></html>",
+        "noQuirksBodyHtml": "<table><tbody><tr><th></th></tr></tbody></table>"
+      }
+    },
+    {
+      "data": "<table><td>",
+      "errors": [
+        "(1,7): expected-doctype-but-got-start-tag",
+        "(1,11): unexpected-cell-in-table-body",
+        "(1,11): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true,
+            "tbody": true,
+            "tr": true,
+            "td": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "tbody",
+                        "children": [
+                          {
+                            "tag": "tr",
+                            "children": [
+                              {
+                                "tag": "td"
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><table><tbody><tr><td></td></tr></tbody></table></body></html>",
+        "noQuirksBodyHtml": "<table><tbody><tr><td></td></tr></tbody></table>"
+      }
+    },
+    {
+      "data": "<table><col foo='bar'>",
+      "errors": [
+        "(1,7): expected-doctype-but-got-start-tag",
+        "(1,22): eof-in-table"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true,
+            "colgroup": true,
+            "col": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "colgroup",
+                        "children": [
+                          {
+                            "tag": "col",
+                            "attrs": [
+                              {
+                                "name": "foo",
+                                "value": "bar"
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><table><colgroup><col foo=\"bar\"></colgroup></table></body></html>",
+        "noQuirksBodyHtml": "<table><colgroup><col foo=\"bar\"></colgroup></table>"
+      }
+    },
+    {
+      "data": "<table><colgroup></html>foo",
+      "errors": [
+        "(1,7): expected-doctype-but-got-start-tag",
+        "(1,24): unexpected-end-tag",
+        "(1,27): foster-parenting-character-in-table",
+        "(1,27): foster-parenting-character-in-table",
+        "(1,27): foster-parenting-character-in-table",
+        "(1,27): eof-in-table"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true,
+            "colgroup": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "foo"
+                  },
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "colgroup"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>foo<table><colgroup></colgroup></table></body></html>",
+        "noQuirksBodyHtml": "foo<table><colgroup></colgroup></table>"
+      }
+    },
+    {
+      "data": "<table></table><p>foo",
+      "errors": [
+        "(1,7): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true,
+            "p": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "table"
+                  },
+                  {
+                    "tag": "p",
+                    "children": [
+                      {
+                        "text": "foo"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><table></table><p>foo</p></body></html>",
+        "noQuirksBodyHtml": "<table></table><p>foo</p>"
+      }
+    },
+    {
+      "data": "<table></body></caption></col></colgroup></html></tbody></td></tfoot></th></thead></tr><td>",
+      "errors": [
+        "(1,7): expected-doctype-but-got-start-tag",
+        "(1,14): unexpected-end-tag",
+        "(1,24): unexpected-end-tag",
+        "(1,30): unexpected-end-tag",
+        "(1,41): unexpected-end-tag",
+        "(1,48): unexpected-end-tag",
+        "(1,56): unexpected-end-tag",
+        "(1,61): unexpected-end-tag",
+        "(1,69): unexpected-end-tag",
+        "(1,74): unexpected-end-tag",
+        "(1,82): unexpected-end-tag",
+        "(1,87): unexpected-end-tag",
+        "(1,91): unexpected-cell-in-table-body",
+        "(1,91): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true,
+            "tbody": true,
+            "tr": true,
+            "td": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "tbody",
+                        "children": [
+                          {
+                            "tag": "tr",
+                            "children": [
+                              {
+                                "tag": "td"
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><table><tbody><tr><td></td></tr></tbody></table></body></html>",
+        "noQuirksBodyHtml": "<table><tbody><tr><td></td></tr></tbody></table>"
+      }
+    },
+    {
+      "data": "<table><select><option>3</select></table>",
+      "errors": [
+        "(1,7): expected-doctype-but-got-start-tag",
+        "(1,15): unexpected-start-tag-implies-table-voodoo"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "select": true,
+            "option": true,
+            "table": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "select",
+                    "children": [
+                      {
+                        "tag": "option",
+                        "children": [
+                          {
+                            "text": "3"
+                          }
+                        ]
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "table"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><select><option>3</option></select><table></table></body></html>",
+        "noQuirksBodyHtml": "<select><option>3</option></select><table></table>"
+      }
+    },
+    {
+      "data": "<table><select><table></table></select></table>",
+      "errors": [
+        "(1,7): expected-doctype-but-got-start-tag",
+        "(1,15): unexpected-start-tag-implies-table-voodoo",
+        "(1,22): unexpected-table-element-start-tag-in-select-in-table",
+        "(1,22): unexpected-start-tag-implies-end-tag",
+        "(1,39): unexpected-end-tag",
+        "(1,47): unexpected-end-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "select": true,
+            "table": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "select"
+                  },
+                  {
+                    "tag": "table"
+                  },
+                  {
+                    "tag": "table"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><select></select><table></table><table></table></body></html>",
+        "noQuirksBodyHtml": "<select></select><table></table><table></table>"
+      }
+    },
+    {
+      "data": "<table><select></table>",
+      "errors": [
+        "(1,7): expected-doctype-but-got-start-tag",
+        "(1,15): unexpected-start-tag-implies-table-voodoo",
+        "(1,23): unexpected-table-element-end-tag-in-select-in-table"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "select": true,
+            "table": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "select"
+                  },
+                  {
+                    "tag": "table"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><select></select><table></table></body></html>",
+        "noQuirksBodyHtml": "<select></select><table></table>"
+      }
+    },
+    {
+      "data": "<table><select><option>A<tr><td>B</td></tr></table>",
+      "errors": [
+        "(1,7): expected-doctype-but-got-start-tag",
+        "(1,15): unexpected-start-tag-implies-table-voodoo",
+        "(1,28): unexpected-table-element-start-tag-in-select-in-table"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "select": true,
+            "option": true,
+            "table": true,
+            "tbody": true,
+            "tr": true,
+            "td": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "select",
+                    "children": [
+                      {
+                        "tag": "option",
+                        "children": [
+                          {
+                            "text": "A"
+                          }
+                        ]
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "tbody",
+                        "children": [
+                          {
+                            "tag": "tr",
+                            "children": [
+                              {
+                                "tag": "td",
+                                "children": [
+                                  {
+                                    "text": "B"
+                                  }
+                                ]
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><select><option>A</option></select><table><tbody><tr><td>B</td></tr></tbody></table></body></html>",
+        "noQuirksBodyHtml": "<select><option>A</option></select><table><tbody><tr><td>B</td></tr></tbody></table>"
+      }
+    },
+    {
+      "data": "<table><td></body></caption></col></colgroup></html>foo",
+      "errors": [
+        "(1,7): expected-doctype-but-got-start-tag",
+        "(1,11): unexpected-cell-in-table-body",
+        "(1,18): unexpected-end-tag",
+        "(1,28): unexpected-end-tag",
+        "(1,34): unexpected-end-tag",
+        "(1,45): unexpected-end-tag",
+        "(1,52): unexpected-end-tag",
+        "(1,55): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true,
+            "tbody": true,
+            "tr": true,
+            "td": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "tbody",
+                        "children": [
+                          {
+                            "tag": "tr",
+                            "children": [
+                              {
+                                "tag": "td",
+                                "children": [
+                                  {
+                                    "text": "foo"
+                                  }
+                                ]
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><table><tbody><tr><td>foo</td></tr></tbody></table></body></html>",
+        "noQuirksBodyHtml": "<table><tbody><tr><td>foo</td></tr></tbody></table>"
+      }
+    },
+    {
+      "data": "<table><td>A</table>B",
+      "errors": [
+        "(1,7): expected-doctype-but-got-start-tag",
+        "(1,11): unexpected-cell-in-table-body"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true,
+            "tbody": true,
+            "tr": true,
+            "td": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "tbody",
+                        "children": [
+                          {
+                            "tag": "tr",
+                            "children": [
+                              {
+                                "tag": "td",
+                                "children": [
+                                  {
+                                    "text": "A"
+                                  }
+                                ]
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  },
+                  {
+                    "text": "B"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><table><tbody><tr><td>A</td></tr></tbody></table>B</body></html>",
+        "noQuirksBodyHtml": "<table><tbody><tr><td>A</td></tr></tbody></table>B"
+      }
+    },
+    {
+      "data": "<table><tr><caption>",
+      "errors": [
+        "(1,7): expected-doctype-but-got-start-tag",
+        "(1,20): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true,
+            "tbody": true,
+            "tr": true,
+            "caption": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "tbody",
+                        "children": [
+                          {
+                            "tag": "tr"
+                          }
+                        ]
+                      },
+                      {
+                        "tag": "caption"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><table><tbody><tr></tr></tbody><caption></caption></table></body></html>",
+        "noQuirksBodyHtml": "<table><tbody><tr></tr></tbody><caption></caption></table>"
+      }
+    },
+    {
+      "data": "<table><tr></body></caption></col></colgroup></html></td></th><td>foo",
+      "errors": [
+        "(1,7): expected-doctype-but-got-start-tag",
+        "(1,18): unexpected-end-tag-in-table-row",
+        "(1,28): unexpected-end-tag-in-table-row",
+        "(1,34): unexpected-end-tag-in-table-row",
+        "(1,45): unexpected-end-tag-in-table-row",
+        "(1,52): unexpected-end-tag-in-table-row",
+        "(1,57): unexpected-end-tag-in-table-row",
+        "(1,62): unexpected-end-tag-in-table-row",
+        "(1,69): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true,
+            "tbody": true,
+            "tr": true,
+            "td": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "tbody",
+                        "children": [
+                          {
+                            "tag": "tr",
+                            "children": [
+                              {
+                                "tag": "td",
+                                "children": [
+                                  {
+                                    "text": "foo"
+                                  }
+                                ]
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><table><tbody><tr><td>foo</td></tr></tbody></table></body></html>",
+        "noQuirksBodyHtml": "<table><tbody><tr><td>foo</td></tr></tbody></table>"
+      }
+    },
+    {
+      "data": "<table><td><tr>",
+      "errors": [
+        "(1,7): expected-doctype-but-got-start-tag",
+        "(1,11): unexpected-cell-in-table-body",
+        "(1,15): eof-in-table"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true,
+            "tbody": true,
+            "tr": true,
+            "td": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "tbody",
+                        "children": [
+                          {
+                            "tag": "tr",
+                            "children": [
+                              {
+                                "tag": "td"
+                              }
+                            ]
+                          },
+                          {
+                            "tag": "tr"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><table><tbody><tr><td></td></tr><tr></tr></tbody></table></body></html>",
+        "noQuirksBodyHtml": "<table><tbody><tr><td></td></tr><tr></tr></tbody></table>"
+      }
+    },
+    {
+      "data": "<table><td><button><td>",
+      "errors": [
+        "(1,7): expected-doctype-but-got-start-tag",
+        "(1,11): unexpected-cell-in-table-body",
+        "(1,23): unexpected-cell-end-tag",
+        "(1,23): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true,
+            "tbody": true,
+            "tr": true,
+            "td": true,
+            "button": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "tbody",
+                        "children": [
+                          {
+                            "tag": "tr",
+                            "children": [
+                              {
+                                "tag": "td",
+                                "children": [
+                                  {
+                                    "tag": "button"
+                                  }
+                                ]
+                              },
+                              {
+                                "tag": "td"
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><table><tbody><tr><td><button></button></td><td></td></tr></tbody></table></body></html>",
+        "noQuirksBodyHtml": "<table><tbody><tr><td><button></button></td><td></td></tr></tbody></table>"
+      }
+    },
+    {
+      "data": "<table><tr><td><svg><desc><td>",
+      "errors": [
+        "(1,7): expected-doctype-but-got-start-tag",
+        "(1,30): unexpected-cell-end-tag",
+        "(1,30): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true,
+            "tbody": true,
+            "tr": true,
+            "td": true,
+            "svg svg": true,
+            "svg desc": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "tbody",
+                        "children": [
+                          {
+                            "tag": "tr",
+                            "children": [
+                              {
+                                "tag": "td",
+                                "children": [
+                                  {
+                                    "tag": "svg",
+                                    "ns": "http://www.w3.org/2000/svg",
+                                    "children": [
+                                      {
+                                        "tag": "desc",
+                                        "ns": "http://www.w3.org/2000/svg"
+                                      }
+                                    ]
+                                  }
+                                ]
+                              },
+                              {
+                                "tag": "td"
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><table><tbody><tr><td><svg><desc></desc></svg></td><td></td></tr></tbody></table></body></html>",
+        "noQuirksBodyHtml": "<table><tbody><tr><td><svg><desc></desc></svg></td><td></td></tr></tbody></table>"
+      }
+    }
+  ],
+  "template.dat": [
+    {
+      "data": "<body><template>Hello</template>",
+      "errors": [
+        "no doctype"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "template": true
+          },
+          "template": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "template",
+                    "children": [
+                      {
+                        "content": true,
+                        "children": [
+                          {
+                            "text": "Hello"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><template>Hello</template></body></html>",
+        "noQuirksBodyHtml": "<template>Hello</template>"
+      }
+    },
+    {
+      "data": "<template>Hello</template>",
+      "errors": [
+        "no doctype"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "template": true,
+            "body": true
+          },
+          "template": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "template",
+                    "children": [
+                      {
+                        "content": true,
+                        "children": [
+                          {
+                            "text": "Hello"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><template>Hello</template></head><body></body></html>",
+        "noQuirksBodyHtml": "<template>Hello</template>"
+      }
+    },
+    {
+      "data": "<template></template><div></div>",
+      "errors": [
+        "no doctype"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "template": true,
+            "body": true,
+            "div": true
+          },
+          "template": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "template",
+                    "children": [
+                      {
+                        "content": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "div"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><template></template></head><body><div></div></body></html>",
+        "noQuirksBodyHtml": "<template></template><div></div>"
+      }
+    },
+    {
+      "data": "<html><template>Hello</template>",
+      "errors": [
+        "no doctype"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "template": true,
+            "body": true
+          },
+          "template": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "template",
+                    "children": [
+                      {
+                        "content": true,
+                        "children": [
+                          {
+                            "text": "Hello"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><template>Hello</template></head><body></body></html>",
+        "noQuirksBodyHtml": "<template>Hello</template>"
+      }
+    },
+    {
+      "data": "<head><template><div></div></template></head>",
+      "errors": [
+        "no doctype"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "template": true,
+            "div": true,
+            "body": true
+          },
+          "template": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "template",
+                    "children": [
+                      {
+                        "content": true,
+                        "children": [
+                          {
+                            "tag": "div"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><template><div></div></template></head><body></body></html>",
+        "noQuirksBodyHtml": "<template><div></div></template>"
+      }
+    },
+    {
+      "data": "<div><template><div><span></template><b>",
+      "errors": [
+        " * (1,6) missing DOCTYPE",
+        " * (1,38) mismatched template end tag",
+        " * (1,41) unexpected end of file"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "div": true,
+            "template": true,
+            "span": true,
+            "b": true
+          },
+          "template": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "div",
+                    "children": [
+                      {
+                        "tag": "template",
+                        "children": [
+                          {
+                            "content": true,
+                            "children": [
+                              {
+                                "tag": "div",
+                                "children": [
+                                  {
+                                    "tag": "span"
+                                  }
+                                ]
+                              }
+                            ]
+                          }
+                        ]
+                      },
+                      {
+                        "tag": "b"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><div><template><div><span></span></div></template><b></b></div></body></html>",
+        "noQuirksBodyHtml": "<div><template><div><span></span></div></template><b></b></div>"
+      }
+    },
+    {
+      "data": "<div><template></div>Hello",
+      "errors": [
+        " * (1,6) missing DOCTYPE",
+        " * (1,22) unexpected token in template",
+        " * (1,27) unexpected end of file in template",
+        " * (1,27) unexpected end of file"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "div": true,
+            "template": true
+          },
+          "template": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "div",
+                    "children": [
+                      {
+                        "tag": "template",
+                        "children": [
+                          {
+                            "content": true,
+                            "children": [
+                              {
+                                "text": "Hello"
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><div><template>Hello</template></div></body></html>",
+        "noQuirksBodyHtml": "<div><template>Hello</template></div>"
+      }
+    },
+    {
+      "data": "<div></template></div>",
+      "errors": [
+        " * (1,6) missing DOCTYPE",
+        " * (1,17) unexpected template end tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "div": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "div"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><div></div></body></html>",
+        "noQuirksBodyHtml": "<div></div>"
+      }
+    },
+    {
+      "data": "<table><template></template></table>",
+      "errors": [
+        "no doctype"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true,
+            "template": true
+          },
+          "template": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "template",
+                        "children": [
+                          {
+                            "content": true
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><table><template></template></table></body></html>",
+        "noQuirksBodyHtml": "<table><template></template></table>"
+      }
+    },
+    {
+      "data": "<table><template></template></div>",
+      "errors": [
+        " * (1,8) missing DOCTYPE",
+        " * (1,35) unexpected token in table - foster parenting",
+        " * (1,35) unexpected end tag",
+        " * (1,35) unexpected end of file"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true,
+            "template": true
+          },
+          "template": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "template",
+                        "children": [
+                          {
+                            "content": true
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><table><template></template></table></body></html>",
+        "noQuirksBodyHtml": "<table><template></template></table>"
+      }
+    },
+    {
+      "data": "<table><div><template></template></div>",
+      "errors": [
+        " * (1,8) missing DOCTYPE",
+        " * (1,13) unexpected token in table - foster parenting",
+        " * (1,40) unexpected token in table - foster parenting",
+        " * (1,40) unexpected end of file"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "div": true,
+            "template": true,
+            "table": true
+          },
+          "template": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "div",
+                    "children": [
+                      {
+                        "tag": "template",
+                        "children": [
+                          {
+                            "content": true
+                          }
+                        ]
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "table"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><div><template></template></div><table></table></body></html>",
+        "noQuirksBodyHtml": "<div><template></template></div><table></table>"
+      }
+    },
+    {
+      "data": "<table><template></template><div></div>",
+      "errors": [
+        "no doctype",
+        "bad div in table",
+        "bad /div in table",
+        "eof in table"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "div": true,
+            "table": true,
+            "template": true
+          },
+          "template": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "div"
+                  },
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "template",
+                        "children": [
+                          {
+                            "content": true
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><div></div><table><template></template></table></body></html>",
+        "noQuirksBodyHtml": "<div></div><table><template></template></table>"
+      }
+    },
+    {
+      "data": "<table>   <template></template></table>",
+      "errors": [
+        "no doctype"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true,
+            "template": true
+          },
+          "template": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "text": "   "
+                      },
+                      {
+                        "tag": "template",
+                        "children": [
+                          {
+                            "content": true
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><table>   <template></template></table></body></html>",
+        "noQuirksBodyHtml": "<table>   <template></template></table>"
+      }
+    },
+    {
+      "data": "<table><tbody><template></template></tbody>",
+      "errors": [
+        "no doctype",
+        "eof in table"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true,
+            "tbody": true,
+            "template": true
+          },
+          "template": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "tbody",
+                        "children": [
+                          {
+                            "tag": "template",
+                            "children": [
+                              {
+                                "content": true
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><table><tbody><template></template></tbody></table></body></html>",
+        "noQuirksBodyHtml": "<table><tbody><template></template></tbody></table>"
+      }
+    },
+    {
+      "data": "<table><tbody><template></tbody></template>",
+      "errors": [
+        "no doctype",
+        "bad /tbody",
+        "eof in table"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true,
+            "tbody": true,
+            "template": true
+          },
+          "template": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "tbody",
+                        "children": [
+                          {
+                            "tag": "template",
+                            "children": [
+                              {
+                                "content": true
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><table><tbody><template></template></tbody></table></body></html>",
+        "noQuirksBodyHtml": "<table><tbody><template></template></tbody></table>"
+      }
+    },
+    {
+      "data": "<table><tbody><template></template></tbody></table>",
+      "errors": [
+        "no doctype"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true,
+            "tbody": true,
+            "template": true
+          },
+          "template": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "tbody",
+                        "children": [
+                          {
+                            "tag": "template",
+                            "children": [
+                              {
+                                "content": true
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><table><tbody><template></template></tbody></table></body></html>",
+        "noQuirksBodyHtml": "<table><tbody><template></template></tbody></table>"
+      }
+    },
+    {
+      "data": "<table><thead><template></template></thead>",
+      "errors": [
+        "no doctype",
+        "eof in table"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true,
+            "thead": true,
+            "template": true
+          },
+          "template": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "thead",
+                        "children": [
+                          {
+                            "tag": "template",
+                            "children": [
+                              {
+                                "content": true
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><table><thead><template></template></thead></table></body></html>",
+        "noQuirksBodyHtml": "<table><thead><template></template></thead></table>"
+      }
+    },
+    {
+      "data": "<table><tfoot><template></template></tfoot>",
+      "errors": [
+        "no doctype",
+        "eof in table"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true,
+            "tfoot": true,
+            "template": true
+          },
+          "template": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "tfoot",
+                        "children": [
+                          {
+                            "tag": "template",
+                            "children": [
+                              {
+                                "content": true
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><table><tfoot><template></template></tfoot></table></body></html>",
+        "noQuirksBodyHtml": "<table><tfoot><template></template></tfoot></table>"
+      }
+    },
+    {
+      "data": "<select><template></template></select>",
+      "errors": [
+        "no doctype"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "select": true,
+            "template": true
+          },
+          "template": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "select",
+                    "children": [
+                      {
+                        "tag": "template",
+                        "children": [
+                          {
+                            "content": true
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><select><template></template></select></body></html>",
+        "noQuirksBodyHtml": "<select><template></template></select>"
+      }
+    },
+    {
+      "data": "<select><template><option></option></template></select>",
+      "errors": [
+        "no doctype"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "select": true,
+            "template": true,
+            "option": true
+          },
+          "template": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "select",
+                    "children": [
+                      {
+                        "tag": "template",
+                        "children": [
+                          {
+                            "content": true,
+                            "children": [
+                              {
+                                "tag": "option"
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><select><template><option></option></template></select></body></html>",
+        "noQuirksBodyHtml": "<select><template><option></option></template></select>"
+      }
+    },
+    {
+      "data": "<template><option></option></select><option></option></template>",
+      "errors": [
+        "no doctype",
+        "bad /select"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "template": true,
+            "option": true,
+            "body": true
+          },
+          "template": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "template",
+                    "children": [
+                      {
+                        "content": true,
+                        "children": [
+                          {
+                            "tag": "option"
+                          },
+                          {
+                            "tag": "option"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><template><option></option><option></option></template></head><body></body></html>",
+        "noQuirksBodyHtml": "<template><option></option><option></option></template>"
+      }
+    },
+    {
+      "data": "<select><template></template><option></select>",
+      "errors": [
+        "no doctype"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "select": true,
+            "template": true,
+            "option": true
+          },
+          "template": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "select",
+                    "children": [
+                      {
+                        "tag": "template",
+                        "children": [
+                          {
+                            "content": true
+                          }
+                        ]
+                      },
+                      {
+                        "tag": "option"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><select><template></template><option></option></select></body></html>",
+        "noQuirksBodyHtml": "<select><template></template><option></option></select>"
+      }
+    },
+    {
+      "data": "<select><option><template></template></select>",
+      "errors": [
+        "no doctype"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "select": true,
+            "option": true,
+            "template": true
+          },
+          "template": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "select",
+                    "children": [
+                      {
+                        "tag": "option",
+                        "children": [
+                          {
+                            "tag": "template",
+                            "children": [
+                              {
+                                "content": true
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><select><option><template></template></option></select></body></html>",
+        "noQuirksBodyHtml": "<select><option><template></template></option></select>"
+      }
+    },
+    {
+      "data": "<select><template>",
+      "errors": [
+        "no doctype",
+        "eof in template",
+        "eof in select"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "select": true,
+            "template": true
+          },
+          "template": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "select",
+                    "children": [
+                      {
+                        "tag": "template",
+                        "children": [
+                          {
+                            "content": true
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><select><template></template></select></body></html>",
+        "noQuirksBodyHtml": "<select><template></template></select>"
+      }
+    },
+    {
+      "data": "<select><option></option><template>",
+      "errors": [
+        "no doctype",
+        "eof in template",
+        "eof in select"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "select": true,
+            "option": true,
+            "template": true
+          },
+          "template": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "select",
+                    "children": [
+                      {
+                        "tag": "option"
+                      },
+                      {
+                        "tag": "template",
+                        "children": [
+                          {
+                            "content": true
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><select><option></option><template></template></select></body></html>",
+        "noQuirksBodyHtml": "<select><option></option><template></template></select>"
+      }
+    },
+    {
+      "data": "<select><option></option><template><option>",
+      "errors": [
+        "no doctype",
+        "eof in template",
+        "eof in select"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "select": true,
+            "option": true,
+            "template": true
+          },
+          "template": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "select",
+                    "children": [
+                      {
+                        "tag": "option"
+                      },
+                      {
+                        "tag": "template",
+                        "children": [
+                          {
+                            "content": true,
+                            "children": [
+                              {
+                                "tag": "option"
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><select><option></option><template><option></option></template></select></body></html>",
+        "noQuirksBodyHtml": "<select><option></option><template><option></option></template></select>"
+      }
+    },
+    {
+      "data": "<table><thead><template><td></template></table>",
+      "errors": [
+        " * (1,8) missing DOCTYPE"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true,
+            "thead": true,
+            "template": true,
+            "td": true
+          },
+          "template": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "thead",
+                        "children": [
+                          {
+                            "tag": "template",
+                            "children": [
+                              {
+                                "content": true,
+                                "children": [
+                                  {
+                                    "tag": "td"
+                                  }
+                                ]
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><table><thead><template><td></td></template></thead></table></body></html>",
+        "noQuirksBodyHtml": "<table><thead><template><td></td></template></thead></table>"
+      }
+    },
+    {
+      "data": "<table><template><thead></template></table>",
+      "errors": [
+        "no doctype"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true,
+            "template": true,
+            "thead": true
+          },
+          "template": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "template",
+                        "children": [
+                          {
+                            "content": true,
+                            "children": [
+                              {
+                                "tag": "thead"
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><table><template><thead></thead></template></table></body></html>",
+        "noQuirksBodyHtml": "<table><template><thead></thead></template></table>"
+      }
+    },
+    {
+      "data": "<body><table><template><td></tr><div></template></table>",
+      "errors": [
+        "no doctype",
+        "bad </tr>",
+        "missing </div>"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true,
+            "template": true,
+            "td": true,
+            "div": true
+          },
+          "template": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "template",
+                        "children": [
+                          {
+                            "content": true,
+                            "children": [
+                              {
+                                "tag": "td",
+                                "children": [
+                                  {
+                                    "tag": "div"
+                                  }
+                                ]
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><table><template><td><div></div></td></template></table></body></html>",
+        "noQuirksBodyHtml": "<table><template><td><div></div></td></template></table>"
+      }
+    },
+    {
+      "data": "<table><template><thead></template></thead></table>",
+      "errors": [
+        "no doctype",
+        "bad /thead after /template"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true,
+            "template": true,
+            "thead": true
+          },
+          "template": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "template",
+                        "children": [
+                          {
+                            "content": true,
+                            "children": [
+                              {
+                                "tag": "thead"
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><table><template><thead></thead></template></table></body></html>",
+        "noQuirksBodyHtml": "<table><template><thead></thead></template></table>"
+      }
+    },
+    {
+      "data": "<table><thead><template><tr></template></table>",
+      "errors": [
+        "no doctype"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true,
+            "thead": true,
+            "template": true,
+            "tr": true
+          },
+          "template": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "thead",
+                        "children": [
+                          {
+                            "tag": "template",
+                            "children": [
+                              {
+                                "content": true,
+                                "children": [
+                                  {
+                                    "tag": "tr"
+                                  }
+                                ]
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><table><thead><template><tr></tr></template></thead></table></body></html>",
+        "noQuirksBodyHtml": "<table><thead><template><tr></tr></template></thead></table>"
+      }
+    },
+    {
+      "data": "<table><template><tr></template></table>",
+      "errors": [
+        "no doctype"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true,
+            "template": true,
+            "tr": true
+          },
+          "template": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "template",
+                        "children": [
+                          {
+                            "content": true,
+                            "children": [
+                              {
+                                "tag": "tr"
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><table><template><tr></tr></template></table></body></html>",
+        "noQuirksBodyHtml": "<table><template><tr></tr></template></table>"
+      }
+    },
+    {
+      "data": "<table><tr><template><td>",
+      "errors": [
+        "no doctype",
+        "eof in template",
+        "eof in table"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true,
+            "tbody": true,
+            "tr": true,
+            "template": true,
+            "td": true
+          },
+          "template": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "tbody",
+                        "children": [
+                          {
+                            "tag": "tr",
+                            "children": [
+                              {
+                                "tag": "template",
+                                "children": [
+                                  {
+                                    "content": true,
+                                    "children": [
+                                      {
+                                        "tag": "td"
+                                      }
+                                    ]
+                                  }
+                                ]
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><table><tbody><tr><template><td></td></template></tr></tbody></table></body></html>",
+        "noQuirksBodyHtml": "<table><tbody><tr><template><td></td></template></tr></tbody></table>"
+      }
+    },
+    {
+      "data": "<table><template><tr><template><td></template></tr></template></table>",
+      "errors": [
+        "no doctype"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true,
+            "template": true,
+            "tr": true,
+            "td": true
+          },
+          "template": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "template",
+                        "children": [
+                          {
+                            "content": true,
+                            "children": [
+                              {
+                                "tag": "tr",
+                                "children": [
+                                  {
+                                    "tag": "template",
+                                    "children": [
+                                      {
+                                        "content": true,
+                                        "children": [
+                                          {
+                                            "tag": "td"
+                                          }
+                                        ]
+                                      }
+                                    ]
+                                  }
+                                ]
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><table><template><tr><template><td></td></template></tr></template></table></body></html>",
+        "noQuirksBodyHtml": "<table><template><tr><template><td></td></template></tr></template></table>"
+      }
+    },
+    {
+      "data": "<table><template><tr><template><td></td></template></tr></template></table>",
+      "errors": [
+        "no doctype"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true,
+            "template": true,
+            "tr": true,
+            "td": true
+          },
+          "template": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "template",
+                        "children": [
+                          {
+                            "content": true,
+                            "children": [
+                              {
+                                "tag": "tr",
+                                "children": [
+                                  {
+                                    "tag": "template",
+                                    "children": [
+                                      {
+                                        "content": true,
+                                        "children": [
+                                          {
+                                            "tag": "td"
+                                          }
+                                        ]
+                                      }
+                                    ]
+                                  }
+                                ]
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><table><template><tr><template><td></td></template></tr></template></table></body></html>",
+        "noQuirksBodyHtml": "<table><template><tr><template><td></td></template></tr></template></table>"
+      }
+    },
+    {
+      "data": "<table><template><td></template>",
+      "errors": [
+        "no doctype",
+        "eof in table"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true,
+            "template": true,
+            "td": true
+          },
+          "template": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "template",
+                        "children": [
+                          {
+                            "content": true,
+                            "children": [
+                              {
+                                "tag": "td"
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><table><template><td></td></template></table></body></html>",
+        "noQuirksBodyHtml": "<table><template><td></td></template></table>"
+      }
+    },
+    {
+      "data": "<body><template><td></td></template>",
+      "errors": [
+        "no doctype"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "template": true,
+            "td": true
+          },
+          "template": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "template",
+                    "children": [
+                      {
+                        "content": true,
+                        "children": [
+                          {
+                            "tag": "td"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><template><td></td></template></body></html>",
+        "noQuirksBodyHtml": "<template><td></td></template>"
+      }
+    },
+    {
+      "data": "<body><template><template><tr></tr></template><td></td></template>",
+      "errors": [
+        "no doctype"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "template": true,
+            "tr": true,
+            "td": true
+          },
+          "template": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "template",
+                    "children": [
+                      {
+                        "content": true,
+                        "children": [
+                          {
+                            "tag": "template",
+                            "children": [
+                              {
+                                "content": true,
+                                "children": [
+                                  {
+                                    "tag": "tr"
+                                  }
+                                ]
+                              }
+                            ]
+                          },
+                          {
+                            "tag": "td"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><template><template><tr></tr></template><td></td></template></body></html>",
+        "noQuirksBodyHtml": "<template><template><tr></tr></template><td></td></template>"
+      }
+    },
+    {
+      "data": "<table><colgroup><template><col>",
+      "errors": [
+        "no doctype",
+        "eof in template",
+        "eof in table"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true,
+            "colgroup": true,
+            "template": true,
+            "col": true
+          },
+          "template": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "colgroup",
+                        "children": [
+                          {
+                            "tag": "template",
+                            "children": [
+                              {
+                                "content": true,
+                                "children": [
+                                  {
+                                    "tag": "col"
+                                  }
+                                ]
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><table><colgroup><template><col></template></colgroup></table></body></html>",
+        "noQuirksBodyHtml": "<table><colgroup><template><col></template></colgroup></table>"
+      }
+    },
+    {
+      "data": "<frameset><template><frame></frame></template></frameset>",
+      "errors": [
+        " * (1,11) missing DOCTYPE",
+        " * (1,21) unexpected start tag token",
+        " * (1,36) unexpected end tag token",
+        " * (1,47) unexpected end tag token"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "frameset": true,
+            "frame": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "frameset",
+                "children": [
+                  {
+                    "tag": "frame"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><frameset><frame></frameset></html>",
+        "noQuirksBodyHtml": "<template></template>"
+      }
+    },
+    {
+      "data": "<template><frame></frame></frameset><frame></frame></template>",
+      "errors": [
+        " * (1,11) missing DOCTYPE",
+        " * (1,18) unexpected start tag",
+        " * (1,26) unexpected end tag",
+        " * (1,37) unexpected end tag",
+        " * (1,44) unexpected start tag",
+        " * (1,52) unexpected end tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "template": true,
+            "body": true
+          },
+          "template": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "template",
+                    "children": [
+                      {
+                        "content": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><template></template></head><body></body></html>",
+        "noQuirksBodyHtml": "<template></template>"
+      }
+    },
+    {
+      "data": "<template><div><frameset><span></span></div><span></span></template>",
+      "errors": [
+        "no doctype",
+        "bad frameset"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "template": true,
+            "div": true,
+            "span": true,
+            "body": true
+          },
+          "template": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "template",
+                    "children": [
+                      {
+                        "content": true,
+                        "children": [
+                          {
+                            "tag": "div",
+                            "children": [
+                              {
+                                "tag": "span"
+                              }
+                            ]
+                          },
+                          {
+                            "tag": "span"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><template><div><span></span></div><span></span></template></head><body></body></html>",
+        "noQuirksBodyHtml": "<template><div><span></span></div><span></span></template>"
+      }
+    },
+    {
+      "data": "<body><template><div><frameset><span></span></div><span></span></template></body>",
+      "errors": [
+        "no doctype",
+        "bad frameset"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "template": true,
+            "div": true,
+            "span": true
+          },
+          "template": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "template",
+                    "children": [
+                      {
+                        "content": true,
+                        "children": [
+                          {
+                            "tag": "div",
+                            "children": [
+                              {
+                                "tag": "span"
+                              }
+                            ]
+                          },
+                          {
+                            "tag": "span"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><template><div><span></span></div><span></span></template></body></html>",
+        "noQuirksBodyHtml": "<template><div><span></span></div><span></span></template>"
+      }
+    },
+    {
+      "data": "<body><template><script>var i = 1;</script><td></td></template>",
+      "errors": [
+        "no doctype"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "template": true,
+            "script": true,
+            "td": true
+          },
+          "template": true,
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "template",
+                    "children": [
+                      {
+                        "content": true,
+                        "children": [
+                          {
+                            "tag": "script",
+                            "children": [
+                              {
+                                "text": "var i = 1;",
+                                "no_escape": true
+                              }
+                            ]
+                          },
+                          {
+                            "tag": "td"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><template><script>var i = 1;</script><td></td></template></body></html>",
+        "noQuirksBodyHtml": "<template><script>var i = 1;</script><td></td></template>"
+      }
+    },
+    {
+      "data": "<body><template><tr><div></div></tr></template>",
+      "errors": [
+        "no doctype",
+        "foster-parented div",
+        "foster-parented /div"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "template": true,
+            "tr": true,
+            "div": true
+          },
+          "template": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "template",
+                    "children": [
+                      {
+                        "content": true,
+                        "children": [
+                          {
+                            "tag": "tr"
+                          },
+                          {
+                            "tag": "div"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><template><tr></tr><div></div></template></body></html>",
+        "noQuirksBodyHtml": "<template><tr></tr><div></div></template>"
+      }
+    },
+    {
+      "data": "<body><template><tr></tr><td></td></template>",
+      "errors": [
+        "no doctype",
+        "unexpected <td>"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "template": true,
+            "tr": true,
+            "td": true
+          },
+          "template": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "template",
+                    "children": [
+                      {
+                        "content": true,
+                        "children": [
+                          {
+                            "tag": "tr"
+                          },
+                          {
+                            "tag": "tr",
+                            "children": [
+                              {
+                                "tag": "td"
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><template><tr></tr><tr><td></td></tr></template></body></html>",
+        "noQuirksBodyHtml": "<template><tr></tr><tr><td></td></tr></template>"
+      }
+    },
+    {
+      "data": "<body><template><td></td></tr><td></td></template>",
+      "errors": [
+        "no doctype",
+        "bad </tr>"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "template": true,
+            "td": true
+          },
+          "template": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "template",
+                    "children": [
+                      {
+                        "content": true,
+                        "children": [
+                          {
+                            "tag": "td"
+                          },
+                          {
+                            "tag": "td"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><template><td></td><td></td></template></body></html>",
+        "noQuirksBodyHtml": "<template><td></td><td></td></template>"
+      }
+    },
+    {
+      "data": "<body><template><td></td><tbody><td></td></template>",
+      "errors": [
+        "no doctype",
+        "bad <tbody>"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "template": true,
+            "td": true
+          },
+          "template": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "template",
+                    "children": [
+                      {
+                        "content": true,
+                        "children": [
+                          {
+                            "tag": "td"
+                          },
+                          {
+                            "tag": "td"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><template><td></td><td></td></template></body></html>",
+        "noQuirksBodyHtml": "<template><td></td><td></td></template>"
+      }
+    },
+    {
+      "data": "<body><template><td></td><caption></caption><td></td></template>",
+      "errors": [
+        " * (1,7) missing DOCTYPE",
+        " * (1,35) unexpected start tag in table row",
+        " * (1,45) unexpected end tag in table row"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "template": true,
+            "td": true
+          },
+          "template": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "template",
+                    "children": [
+                      {
+                        "content": true,
+                        "children": [
+                          {
+                            "tag": "td"
+                          },
+                          {
+                            "tag": "td"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><template><td></td><td></td></template></body></html>",
+        "noQuirksBodyHtml": "<template><td></td><td></td></template>"
+      }
+    },
+    {
+      "data": "<body><template><td></td><colgroup></caption><td></td></template>",
+      "errors": [
+        " * (1,7) missing DOCTYPE",
+        " * (1,36) unexpected start tag in table row",
+        " * (1,46) unexpected end tag in table row"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "template": true,
+            "td": true
+          },
+          "template": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "template",
+                    "children": [
+                      {
+                        "content": true,
+                        "children": [
+                          {
+                            "tag": "td"
+                          },
+                          {
+                            "tag": "td"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><template><td></td><td></td></template></body></html>",
+        "noQuirksBodyHtml": "<template><td></td><td></td></template>"
+      }
+    },
+    {
+      "data": "<body><template><td></td></table><td></td></template>",
+      "errors": [
+        "no doctype",
+        "bad </table>"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "template": true,
+            "td": true
+          },
+          "template": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "template",
+                    "children": [
+                      {
+                        "content": true,
+                        "children": [
+                          {
+                            "tag": "td"
+                          },
+                          {
+                            "tag": "td"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><template><td></td><td></td></template></body></html>",
+        "noQuirksBodyHtml": "<template><td></td><td></td></template>"
+      }
+    },
+    {
+      "data": "<body><template><tr></tr><tbody><tr></tr></template>",
+      "errors": [
+        "no doctype",
+        "bad <tbody>"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "template": true,
+            "tr": true
+          },
+          "template": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "template",
+                    "children": [
+                      {
+                        "content": true,
+                        "children": [
+                          {
+                            "tag": "tr"
+                          },
+                          {
+                            "tag": "tr"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><template><tr></tr><tr></tr></template></body></html>",
+        "noQuirksBodyHtml": "<template><tr></tr><tr></tr></template>"
+      }
+    },
+    {
+      "data": "<body><template><tr></tr><caption><tr></tr></template>",
+      "errors": [
+        "no doctype",
+        "bad <caption>"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "template": true,
+            "tr": true
+          },
+          "template": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "template",
+                    "children": [
+                      {
+                        "content": true,
+                        "children": [
+                          {
+                            "tag": "tr"
+                          },
+                          {
+                            "tag": "tr"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><template><tr></tr><tr></tr></template></body></html>",
+        "noQuirksBodyHtml": "<template><tr></tr><tr></tr></template>"
+      }
+    },
+    {
+      "data": "<body><template><tr></tr></table><tr></tr></template>",
+      "errors": [
+        "no doctype",
+        "bad </table>"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "template": true,
+            "tr": true
+          },
+          "template": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "template",
+                    "children": [
+                      {
+                        "content": true,
+                        "children": [
+                          {
+                            "tag": "tr"
+                          },
+                          {
+                            "tag": "tr"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><template><tr></tr><tr></tr></template></body></html>",
+        "noQuirksBodyHtml": "<template><tr></tr><tr></tr></template>"
+      }
+    },
+    {
+      "data": "<body><template><thead></thead><caption></caption><tbody></tbody></template>",
+      "errors": [
+        "no doctype"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "template": true,
+            "thead": true,
+            "caption": true,
+            "tbody": true
+          },
+          "template": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "template",
+                    "children": [
+                      {
+                        "content": true,
+                        "children": [
+                          {
+                            "tag": "thead"
+                          },
+                          {
+                            "tag": "caption"
+                          },
+                          {
+                            "tag": "tbody"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><template><thead></thead><caption></caption><tbody></tbody></template></body></html>",
+        "noQuirksBodyHtml": "<template><thead></thead><caption></caption><tbody></tbody></template>"
+      }
+    },
+    {
+      "data": "<body><template><thead></thead></table><tbody></tbody></template></body>",
+      "errors": [
+        "no doctype",
+        "bad </table>"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "template": true,
+            "thead": true,
+            "tbody": true
+          },
+          "template": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "template",
+                    "children": [
+                      {
+                        "content": true,
+                        "children": [
+                          {
+                            "tag": "thead"
+                          },
+                          {
+                            "tag": "tbody"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><template><thead></thead><tbody></tbody></template></body></html>",
+        "noQuirksBodyHtml": "<template><thead></thead><tbody></tbody></template>"
+      }
+    },
+    {
+      "data": "<body><template><div><tr></tr></div></template>",
+      "errors": [
+        "no doctype",
+        "bad tr",
+        "bad /tr"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "template": true,
+            "div": true
+          },
+          "template": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "template",
+                    "children": [
+                      {
+                        "content": true,
+                        "children": [
+                          {
+                            "tag": "div"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><template><div></div></template></body></html>",
+        "noQuirksBodyHtml": "<template><div></div></template>"
+      }
+    },
+    {
+      "data": "<body><template><em>Hello</em></template>",
+      "errors": [
+        "no doctype"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "template": true,
+            "em": true
+          },
+          "template": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "template",
+                    "children": [
+                      {
+                        "content": true,
+                        "children": [
+                          {
+                            "tag": "em",
+                            "children": [
+                              {
+                                "text": "Hello"
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><template><em>Hello</em></template></body></html>",
+        "noQuirksBodyHtml": "<template><em>Hello</em></template>"
+      }
+    },
+    {
+      "data": "<body><template><!--comment--></template>",
+      "errors": [
+        "no doctype"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "template": true
+          },
+          "template": true,
+          "comment": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "template",
+                    "children": [
+                      {
+                        "content": true,
+                        "children": [
+                          {
+                            "comment": "comment"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><template><!--comment--></template></body></html>",
+        "noQuirksBodyHtml": "<template><!--comment--></template>"
+      }
+    },
+    {
+      "data": "<body><template><style></style><td></td></template>",
+      "errors": [
+        "no doctype"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "template": true,
+            "style": true,
+            "td": true
+          },
+          "template": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "template",
+                    "children": [
+                      {
+                        "content": true,
+                        "children": [
+                          {
+                            "tag": "style"
+                          },
+                          {
+                            "tag": "td"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><template><style></style><td></td></template></body></html>",
+        "noQuirksBodyHtml": "<template><style></style><td></td></template>"
+      }
+    },
+    {
+      "data": "<body><template><meta><td></td></template>",
+      "errors": [
+        "no doctype"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "template": true,
+            "meta": true,
+            "td": true
+          },
+          "template": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "template",
+                    "children": [
+                      {
+                        "content": true,
+                        "children": [
+                          {
+                            "tag": "meta"
+                          },
+                          {
+                            "tag": "td"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><template><meta><td></td></template></body></html>",
+        "noQuirksBodyHtml": "<template><meta><td></td></template>"
+      }
+    },
+    {
+      "data": "<body><template><link><td></td></template>",
+      "errors": [
+        "no doctype"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "template": true,
+            "link": true,
+            "td": true
+          },
+          "template": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "template",
+                    "children": [
+                      {
+                        "content": true,
+                        "children": [
+                          {
+                            "tag": "link"
+                          },
+                          {
+                            "tag": "td"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><template><link><td></td></template></body></html>",
+        "noQuirksBodyHtml": "<template><link><td></td></template>"
+      }
+    },
+    {
+      "data": "<body><template><template><tr></tr></template><td></td></template>",
+      "errors": [
+        "no doctype"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "template": true,
+            "tr": true,
+            "td": true
+          },
+          "template": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "template",
+                    "children": [
+                      {
+                        "content": true,
+                        "children": [
+                          {
+                            "tag": "template",
+                            "children": [
+                              {
+                                "content": true,
+                                "children": [
+                                  {
+                                    "tag": "tr"
+                                  }
+                                ]
+                              }
+                            ]
+                          },
+                          {
+                            "tag": "td"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><template><template><tr></tr></template><td></td></template></body></html>",
+        "noQuirksBodyHtml": "<template><template><tr></tr></template><td></td></template>"
+      }
+    },
+    {
+      "data": "<body><table><colgroup><template><col></col></template></colgroup></table></body>",
+      "errors": [
+        "no doctype",
+        "bad /col"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true,
+            "colgroup": true,
+            "template": true,
+            "col": true
+          },
+          "template": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "colgroup",
+                        "children": [
+                          {
+                            "tag": "template",
+                            "children": [
+                              {
+                                "content": true,
+                                "children": [
+                                  {
+                                    "tag": "col"
+                                  }
+                                ]
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><table><colgroup><template><col></template></colgroup></table></body></html>",
+        "noQuirksBodyHtml": "<table><colgroup><template><col></template></colgroup></table>"
+      }
+    },
+    {
+      "data": "<body a=b><template><div></div><body c=d><div></div></body></template></body>",
+      "errors": [
+        "no doctype",
+        "bad <body>",
+        "bad </body>"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "template": true,
+            "div": true
+          },
+          "template": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "attrs": [
+                  {
+                    "name": "a",
+                    "value": "b"
+                  }
+                ],
+                "children": [
+                  {
+                    "tag": "template",
+                    "children": [
+                      {
+                        "content": true,
+                        "children": [
+                          {
+                            "tag": "div"
+                          },
+                          {
+                            "tag": "div"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body a=\"b\"><template><div></div><div></div></template></body></html>",
+        "noQuirksBodyHtml": "<template><div></div><div></div></template>"
+      }
+    },
+    {
+      "data": "<html a=b><template><div><html b=c><span></template>",
+      "errors": [
+        "no doctype",
+        "bad <html>",
+        "missing end tags in template"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "template": true,
+            "div": true,
+            "span": true,
+            "body": true
+          },
+          "template": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "attrs": [
+              {
+                "name": "a",
+                "value": "b"
+              }
+            ],
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "template",
+                    "children": [
+                      {
+                        "content": true,
+                        "children": [
+                          {
+                            "tag": "div",
+                            "children": [
+                              {
+                                "tag": "span"
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html a=\"b\"><head><template><div><span></span></div></template></head><body></body></html>",
+        "noQuirksBodyHtml": "<template><div><span></span></div></template>"
+      }
+    },
+    {
+      "data": "<html a=b><template><col></col><html b=c><col></col></template>",
+      "errors": [
+        "no doctype",
+        "bad /col",
+        "bad html",
+        "bad /col"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "template": true,
+            "col": true,
+            "body": true
+          },
+          "template": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "attrs": [
+              {
+                "name": "a",
+                "value": "b"
+              }
+            ],
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "template",
+                    "children": [
+                      {
+                        "content": true,
+                        "children": [
+                          {
+                            "tag": "col"
+                          },
+                          {
+                            "tag": "col"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html a=\"b\"><head><template><col><col></template></head><body></body></html>",
+        "noQuirksBodyHtml": "<template><col><col></template>"
+      }
+    },
+    {
+      "data": "<html a=b><template><frame></frame><html b=c><frame></frame></template>",
+      "errors": [
+        "no doctype",
+        "bad frame",
+        "bad /frame",
+        "bad html",
+        "bad frame",
+        "bad /frame"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "template": true,
+            "body": true
+          },
+          "template": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "attrs": [
+              {
+                "name": "a",
+                "value": "b"
+              }
+            ],
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "template",
+                    "children": [
+                      {
+                        "content": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html a=\"b\"><head><template></template></head><body></body></html>",
+        "noQuirksBodyHtml": "<template></template>"
+      }
+    },
+    {
+      "data": "<body><template><tr></tr><template></template><td></td></template>",
+      "errors": [
+        "no doctype",
+        "unexpected <td>"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "template": true,
+            "tr": true,
+            "td": true
+          },
+          "template": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "template",
+                    "children": [
+                      {
+                        "content": true,
+                        "children": [
+                          {
+                            "tag": "tr"
+                          },
+                          {
+                            "tag": "template",
+                            "children": [
+                              {
+                                "content": true
+                              }
+                            ]
+                          },
+                          {
+                            "tag": "tr",
+                            "children": [
+                              {
+                                "tag": "td"
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><template><tr></tr><template></template><tr><td></td></tr></template></body></html>",
+        "noQuirksBodyHtml": "<template><tr></tr><template></template><tr><td></td></tr></template>"
+      }
+    },
+    {
+      "data": "<body><template><thead></thead><template><tr></tr></template><tr></tr><tfoot></tfoot></template>",
+      "errors": [
+        "no doctype"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "template": true,
+            "thead": true,
+            "tr": true,
+            "tbody": true,
+            "tfoot": true
+          },
+          "template": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "template",
+                    "children": [
+                      {
+                        "content": true,
+                        "children": [
+                          {
+                            "tag": "thead"
+                          },
+                          {
+                            "tag": "template",
+                            "children": [
+                              {
+                                "content": true,
+                                "children": [
+                                  {
+                                    "tag": "tr"
+                                  }
+                                ]
+                              }
+                            ]
+                          },
+                          {
+                            "tag": "tbody",
+                            "children": [
+                              {
+                                "tag": "tr"
+                              }
+                            ]
+                          },
+                          {
+                            "tag": "tfoot"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><template><thead></thead><template><tr></tr></template><tbody><tr></tr></tbody><tfoot></tfoot></template></body></html>",
+        "noQuirksBodyHtml": "<template><thead></thead><template><tr></tr></template><tbody><tr></tr></tbody><tfoot></tfoot></template>"
+      }
+    },
+    {
+      "data": "<body><template><template><b><template></template></template>text</template>",
+      "errors": [
+        "no doctype",
+        "missing </b>"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "template": true,
+            "b": true
+          },
+          "template": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "template",
+                    "children": [
+                      {
+                        "content": true,
+                        "children": [
+                          {
+                            "tag": "template",
+                            "children": [
+                              {
+                                "content": true,
+                                "children": [
+                                  {
+                                    "tag": "b",
+                                    "children": [
+                                      {
+                                        "tag": "template",
+                                        "children": [
+                                          {
+                                            "content": true
+                                          }
+                                        ]
+                                      }
+                                    ]
+                                  }
+                                ]
+                              }
+                            ]
+                          },
+                          {
+                            "text": "text"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><template><template><b><template></template></b></template>text</template></body></html>",
+        "noQuirksBodyHtml": "<template><template><b><template></template></b></template>text</template>"
+      }
+    },
+    {
+      "data": "<body><template><col><colgroup>",
+      "errors": [
+        "no doctype",
+        "bad colgroup",
+        "eof in template"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "template": true,
+            "col": true
+          },
+          "template": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "template",
+                    "children": [
+                      {
+                        "content": true,
+                        "children": [
+                          {
+                            "tag": "col"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><template><col></template></body></html>",
+        "noQuirksBodyHtml": "<template><col></template>"
+      }
+    },
+    {
+      "data": "<body><template><col></colgroup>",
+      "errors": [
+        "no doctype",
+        "bogus /colgroup",
+        "eof in template"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "template": true,
+            "col": true
+          },
+          "template": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "template",
+                    "children": [
+                      {
+                        "content": true,
+                        "children": [
+                          {
+                            "tag": "col"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><template><col></template></body></html>",
+        "noQuirksBodyHtml": "<template><col></template>"
+      }
+    },
+    {
+      "data": "<body><template><col><colgroup></template></body>",
+      "errors": [
+        "no doctype",
+        "bad colgroup"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "template": true,
+            "col": true
+          },
+          "template": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "template",
+                    "children": [
+                      {
+                        "content": true,
+                        "children": [
+                          {
+                            "tag": "col"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><template><col></template></body></html>",
+        "noQuirksBodyHtml": "<template><col></template>"
+      }
+    },
+    {
+      "data": "<body><template><col><div>",
+      "errors": [
+        " * (1,7) missing DOCTYPE",
+        " * (1,27) unexpected token",
+        " * (1,27) unexpected end of file in template"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "template": true,
+            "col": true
+          },
+          "template": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "template",
+                    "children": [
+                      {
+                        "content": true,
+                        "children": [
+                          {
+                            "tag": "col"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><template><col></template></body></html>",
+        "noQuirksBodyHtml": "<template><col></template>"
+      }
+    },
+    {
+      "data": "<body><template><col></div>",
+      "errors": [
+        "no doctype",
+        "bad /div",
+        "eof in template"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "template": true,
+            "col": true
+          },
+          "template": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "template",
+                    "children": [
+                      {
+                        "content": true,
+                        "children": [
+                          {
+                            "tag": "col"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><template><col></template></body></html>",
+        "noQuirksBodyHtml": "<template><col></template>"
+      }
+    },
+    {
+      "data": "<body><template><col>Hello",
+      "errors": [
+        "no doctype",
+        "unexpected text",
+        "eof in template"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "template": true,
+            "col": true
+          },
+          "template": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "template",
+                    "children": [
+                      {
+                        "content": true,
+                        "children": [
+                          {
+                            "tag": "col"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><template><col></template></body></html>",
+        "noQuirksBodyHtml": "<template><col></template>"
+      }
+    },
+    {
+      "data": "<body><template><i><menu>Foo</i>",
+      "errors": [
+        "no doctype",
+        "mising /menu",
+        "eof in template"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "template": true,
+            "i": true,
+            "menu": true
+          },
+          "template": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "template",
+                    "children": [
+                      {
+                        "content": true,
+                        "children": [
+                          {
+                            "tag": "i"
+                          },
+                          {
+                            "tag": "menu",
+                            "children": [
+                              {
+                                "tag": "i",
+                                "children": [
+                                  {
+                                    "text": "Foo"
+                                  }
+                                ]
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><template><i></i><menu><i>Foo</i></menu></template></body></html>",
+        "noQuirksBodyHtml": "<template><i></i><menu><i>Foo</i></menu></template>"
+      }
+    },
+    {
+      "data": "<body><template></div><div>Foo</div><template></template><tr></tr>",
+      "errors": [
+        "no doctype",
+        "bogus /div",
+        "bogus tr",
+        "bogus /tr",
+        "eof in template"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "template": true,
+            "div": true
+          },
+          "template": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "template",
+                    "children": [
+                      {
+                        "content": true,
+                        "children": [
+                          {
+                            "tag": "div",
+                            "children": [
+                              {
+                                "text": "Foo"
+                              }
+                            ]
+                          },
+                          {
+                            "tag": "template",
+                            "children": [
+                              {
+                                "content": true
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><template><div>Foo</div><template></template></template></body></html>",
+        "noQuirksBodyHtml": "<template><div>Foo</div><template></template></template>"
+      }
+    },
+    {
+      "data": "<body><div><template></div><tr><td>Foo</td></tr></template>",
+      "errors": [
+        " * (1,7) missing DOCTYPE",
+        " * (1,28) unexpected token in template",
+        " * (1,60) unexpected end of file"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "div": true,
+            "template": true,
+            "tr": true,
+            "td": true
+          },
+          "template": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "div",
+                    "children": [
+                      {
+                        "tag": "template",
+                        "children": [
+                          {
+                            "content": true,
+                            "children": [
+                              {
+                                "tag": "tr",
+                                "children": [
+                                  {
+                                    "tag": "td",
+                                    "children": [
+                                      {
+                                        "text": "Foo"
+                                      }
+                                    ]
+                                  }
+                                ]
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><div><template><tr><td>Foo</td></tr></template></div></body></html>",
+        "noQuirksBodyHtml": "<div><template><tr><td>Foo</td></tr></template></div>"
+      }
+    },
+    {
+      "data": "<template></figcaption><sub><table></table>",
+      "errors": [
+        "no doctype",
+        "bad /figcaption",
+        "eof in template"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "template": true,
+            "sub": true,
+            "table": true,
+            "body": true
+          },
+          "template": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "template",
+                    "children": [
+                      {
+                        "content": true,
+                        "children": [
+                          {
+                            "tag": "sub",
+                            "children": [
+                              {
+                                "tag": "table"
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><template><sub><table></table></sub></template></head><body></body></html>",
+        "noQuirksBodyHtml": "<template><sub><table></table></sub></template>"
+      }
+    },
+    {
+      "data": "<template><template>",
+      "errors": [
+        "no doctype",
+        "eof in template",
+        "eof in template"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "template": true,
+            "body": true
+          },
+          "template": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "template",
+                    "children": [
+                      {
+                        "content": true,
+                        "children": [
+                          {
+                            "tag": "template",
+                            "children": [
+                              {
+                                "content": true
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><template><template></template></template></head><body></body></html>",
+        "noQuirksBodyHtml": "<template><template></template></template>"
+      }
+    },
+    {
+      "data": "<template><div>",
+      "errors": [
+        "no doctype",
+        "eof in template"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "template": true,
+            "div": true,
+            "body": true
+          },
+          "template": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "template",
+                    "children": [
+                      {
+                        "content": true,
+                        "children": [
+                          {
+                            "tag": "div"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><template><div></div></template></head><body></body></html>",
+        "noQuirksBodyHtml": "<template><div></div></template>"
+      }
+    },
+    {
+      "data": "<template><template><div>",
+      "errors": [
+        "no doctype",
+        "eof in template",
+        "eof in template"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "template": true,
+            "div": true,
+            "body": true
+          },
+          "template": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "template",
+                    "children": [
+                      {
+                        "content": true,
+                        "children": [
+                          {
+                            "tag": "template",
+                            "children": [
+                              {
+                                "content": true,
+                                "children": [
+                                  {
+                                    "tag": "div"
+                                  }
+                                ]
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><template><template><div></div></template></template></head><body></body></html>",
+        "noQuirksBodyHtml": "<template><template><div></div></template></template>"
+      }
+    },
+    {
+      "data": "<template><template><table>",
+      "errors": [
+        "no doctype",
+        "eof in template",
+        "eof in template"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "template": true,
+            "table": true,
+            "body": true
+          },
+          "template": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "template",
+                    "children": [
+                      {
+                        "content": true,
+                        "children": [
+                          {
+                            "tag": "template",
+                            "children": [
+                              {
+                                "content": true,
+                                "children": [
+                                  {
+                                    "tag": "table"
+                                  }
+                                ]
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><template><template><table></table></template></template></head><body></body></html>",
+        "noQuirksBodyHtml": "<template><template><table></table></template></template>"
+      }
+    },
+    {
+      "data": "<template><template><tbody>",
+      "errors": [
+        "no doctype",
+        "eof in template",
+        "eof in template"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "template": true,
+            "tbody": true,
+            "body": true
+          },
+          "template": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "template",
+                    "children": [
+                      {
+                        "content": true,
+                        "children": [
+                          {
+                            "tag": "template",
+                            "children": [
+                              {
+                                "content": true,
+                                "children": [
+                                  {
+                                    "tag": "tbody"
+                                  }
+                                ]
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><template><template><tbody></tbody></template></template></head><body></body></html>",
+        "noQuirksBodyHtml": "<template><template><tbody></tbody></template></template>"
+      }
+    },
+    {
+      "data": "<template><template><tr>",
+      "errors": [
+        "no doctype",
+        "eof in template",
+        "eof in template"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "template": true,
+            "tr": true,
+            "body": true
+          },
+          "template": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "template",
+                    "children": [
+                      {
+                        "content": true,
+                        "children": [
+                          {
+                            "tag": "template",
+                            "children": [
+                              {
+                                "content": true,
+                                "children": [
+                                  {
+                                    "tag": "tr"
+                                  }
+                                ]
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><template><template><tr></tr></template></template></head><body></body></html>",
+        "noQuirksBodyHtml": "<template><template><tr></tr></template></template>"
+      }
+    },
+    {
+      "data": "<template><template><td>",
+      "errors": [
+        "no doctype",
+        "eof in template",
+        "eof in template"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "template": true,
+            "td": true,
+            "body": true
+          },
+          "template": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "template",
+                    "children": [
+                      {
+                        "content": true,
+                        "children": [
+                          {
+                            "tag": "template",
+                            "children": [
+                              {
+                                "content": true,
+                                "children": [
+                                  {
+                                    "tag": "td"
+                                  }
+                                ]
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><template><template><td></td></template></template></head><body></body></html>",
+        "noQuirksBodyHtml": "<template><template><td></td></template></template>"
+      }
+    },
+    {
+      "data": "<template><template><caption>",
+      "errors": [
+        "no doctype",
+        "eof in template",
+        "eof in template"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "template": true,
+            "caption": true,
+            "body": true
+          },
+          "template": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "template",
+                    "children": [
+                      {
+                        "content": true,
+                        "children": [
+                          {
+                            "tag": "template",
+                            "children": [
+                              {
+                                "content": true,
+                                "children": [
+                                  {
+                                    "tag": "caption"
+                                  }
+                                ]
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><template><template><caption></caption></template></template></head><body></body></html>",
+        "noQuirksBodyHtml": "<template><template><caption></caption></template></template>"
+      }
+    },
+    {
+      "data": "<template><template><colgroup>",
+      "errors": [
+        "no doctype",
+        "eof in template",
+        "eof in template"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "template": true,
+            "colgroup": true,
+            "body": true
+          },
+          "template": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "template",
+                    "children": [
+                      {
+                        "content": true,
+                        "children": [
+                          {
+                            "tag": "template",
+                            "children": [
+                              {
+                                "content": true,
+                                "children": [
+                                  {
+                                    "tag": "colgroup"
+                                  }
+                                ]
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><template><template><colgroup></colgroup></template></template></head><body></body></html>",
+        "noQuirksBodyHtml": "<template><template><colgroup></colgroup></template></template>"
+      }
+    },
+    {
+      "data": "<template><template><col>",
+      "errors": [
+        "no doctype",
+        "eof in template",
+        "eof in template"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "template": true,
+            "col": true,
+            "body": true
+          },
+          "template": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "template",
+                    "children": [
+                      {
+                        "content": true,
+                        "children": [
+                          {
+                            "tag": "template",
+                            "children": [
+                              {
+                                "content": true,
+                                "children": [
+                                  {
+                                    "tag": "col"
+                                  }
+                                ]
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><template><template><col></template></template></head><body></body></html>",
+        "noQuirksBodyHtml": "<template><template><col></template></template>"
+      }
+    },
+    {
+      "data": "<template><template><tbody><select>",
+      "errors": [
+        " * (1,11) missing DOCTYPE",
+        " * (1,36) unexpected token in table - foster parenting",
+        " * (1,36) unexpected end of file in template",
+        " * (1,36) unexpected end of file in template"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "template": true,
+            "tbody": true,
+            "select": true,
+            "body": true
+          },
+          "template": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "template",
+                    "children": [
+                      {
+                        "content": true,
+                        "children": [
+                          {
+                            "tag": "template",
+                            "children": [
+                              {
+                                "content": true,
+                                "children": [
+                                  {
+                                    "tag": "tbody"
+                                  },
+                                  {
+                                    "tag": "select"
+                                  }
+                                ]
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><template><template><tbody></tbody><select></select></template></template></head><body></body></html>",
+        "noQuirksBodyHtml": "<template><template><tbody></tbody><select></select></template></template>"
+      }
+    },
+    {
+      "data": "<template><template><table>Foo",
+      "errors": [
+        "no doctype",
+        "foster-parenting text F",
+        "foster-parenting text o",
+        "foster-parenting text o",
+        "eof",
+        "eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "template": true,
+            "table": true,
+            "body": true
+          },
+          "template": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "template",
+                    "children": [
+                      {
+                        "content": true,
+                        "children": [
+                          {
+                            "tag": "template",
+                            "children": [
+                              {
+                                "content": true,
+                                "children": [
+                                  {
+                                    "text": "Foo"
+                                  },
+                                  {
+                                    "tag": "table"
+                                  }
+                                ]
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><template><template>Foo<table></table></template></template></head><body></body></html>",
+        "noQuirksBodyHtml": "<template><template>Foo<table></table></template></template>"
+      }
+    },
+    {
+      "data": "<template><template><frame>",
+      "errors": [
+        "no doctype",
+        "bad tag",
+        "eof",
+        "eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "template": true,
+            "body": true
+          },
+          "template": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "template",
+                    "children": [
+                      {
+                        "content": true,
+                        "children": [
+                          {
+                            "tag": "template",
+                            "children": [
+                              {
+                                "content": true
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><template><template></template></template></head><body></body></html>",
+        "noQuirksBodyHtml": "<template><template></template></template>"
+      }
+    },
+    {
+      "data": "<template><template><script>var i",
+      "errors": [
+        "no doctype",
+        "eof in script",
+        "eof in template",
+        "eof in template"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "template": true,
+            "script": true,
+            "body": true
+          },
+          "template": true,
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "template",
+                    "children": [
+                      {
+                        "content": true,
+                        "children": [
+                          {
+                            "tag": "template",
+                            "children": [
+                              {
+                                "content": true,
+                                "children": [
+                                  {
+                                    "tag": "script",
+                                    "children": [
+                                      {
+                                        "text": "var i",
+                                        "no_escape": true
+                                      }
+                                    ]
+                                  }
+                                ]
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><template><template><script>var i</script></template></template></head><body></body></html>",
+        "noQuirksBodyHtml": "<template><template><script>var i</script></template></template>"
+      }
+    },
+    {
+      "data": "<template><template><style>var i",
+      "errors": [
+        "no doctype",
+        "eof in style",
+        "eof in template",
+        "eof in template"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "template": true,
+            "style": true,
+            "body": true
+          },
+          "template": true,
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "template",
+                    "children": [
+                      {
+                        "content": true,
+                        "children": [
+                          {
+                            "tag": "template",
+                            "children": [
+                              {
+                                "content": true,
+                                "children": [
+                                  {
+                                    "tag": "style",
+                                    "children": [
+                                      {
+                                        "text": "var i",
+                                        "no_escape": true
+                                      }
+                                    ]
+                                  }
+                                ]
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><template><template><style>var i</style></template></template></head><body></body></html>",
+        "noQuirksBodyHtml": "<template><template><style>var i</style></template></template>"
+      }
+    },
+    {
+      "data": "<template><table></template><body><span>Foo",
+      "errors": [
+        "no doctype",
+        "missing /table",
+        "bad eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "template": true,
+            "table": true,
+            "body": true,
+            "span": true
+          },
+          "template": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "template",
+                    "children": [
+                      {
+                        "content": true,
+                        "children": [
+                          {
+                            "tag": "table"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "span",
+                    "children": [
+                      {
+                        "text": "Foo"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><template><table></table></template></head><body><span>Foo</span></body></html>",
+        "noQuirksBodyHtml": "<template><table></table></template><span>Foo</span>"
+      }
+    },
+    {
+      "data": "<template><td></template><body><span>Foo",
+      "errors": [
+        "no doctype",
+        "bad eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "template": true,
+            "td": true,
+            "body": true,
+            "span": true
+          },
+          "template": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "template",
+                    "children": [
+                      {
+                        "content": true,
+                        "children": [
+                          {
+                            "tag": "td"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "span",
+                    "children": [
+                      {
+                        "text": "Foo"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><template><td></td></template></head><body><span>Foo</span></body></html>",
+        "noQuirksBodyHtml": "<template><td></td></template><span>Foo</span>"
+      }
+    },
+    {
+      "data": "<template><object></template><body><span>Foo",
+      "errors": [
+        "no doctype",
+        "missing /object",
+        "bad eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "template": true,
+            "object": true,
+            "body": true,
+            "span": true
+          },
+          "template": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "template",
+                    "children": [
+                      {
+                        "content": true,
+                        "children": [
+                          {
+                            "tag": "object"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "span",
+                    "children": [
+                      {
+                        "text": "Foo"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><template><object></object></template></head><body><span>Foo</span></body></html>",
+        "noQuirksBodyHtml": "<template><object></object></template><span>Foo</span>"
+      }
+    },
+    {
+      "data": "<template><svg><template>",
+      "errors": [
+        "no doctype",
+        "eof in template"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "template": true,
+            "svg svg": true,
+            "svg template": true,
+            "body": true
+          },
+          "template": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "template",
+                    "children": [
+                      {
+                        "content": true,
+                        "children": [
+                          {
+                            "tag": "svg",
+                            "ns": "http://www.w3.org/2000/svg",
+                            "children": [
+                              {
+                                "tag": "template",
+                                "ns": "http://www.w3.org/2000/svg"
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><template><svg><template></template></svg></template></head><body></body></html>",
+        "noQuirksBodyHtml": "<template><svg><template></template></svg></template>"
+      }
+    },
+    {
+      "data": "<template><svg><foo><template><foreignObject><div></template><div>",
+      "errors": [
+        "no doctype",
+        "ugly template closure",
+        "bad eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "template": true,
+            "svg svg": true,
+            "svg foo": true,
+            "svg template": true,
+            "svg foreignObject": true,
+            "div": true,
+            "body": true
+          },
+          "template": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "template",
+                    "children": [
+                      {
+                        "content": true,
+                        "children": [
+                          {
+                            "tag": "svg",
+                            "ns": "http://www.w3.org/2000/svg",
+                            "children": [
+                              {
+                                "tag": "foo",
+                                "ns": "http://www.w3.org/2000/svg",
+                                "children": [
+                                  {
+                                    "tag": "template",
+                                    "ns": "http://www.w3.org/2000/svg",
+                                    "children": [
+                                      {
+                                        "tag": "foreignObject",
+                                        "ns": "http://www.w3.org/2000/svg",
+                                        "children": [
+                                          {
+                                            "tag": "div"
+                                          }
+                                        ]
+                                      }
+                                    ]
+                                  }
+                                ]
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "div"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><template><svg><foo><template><foreignObject><div></div></foreignObject></template></foo></svg></template></head><body><div></div></body></html>",
+        "noQuirksBodyHtml": "<template><svg><foo><template><foreignObject><div></div></foreignObject></template></foo></svg></template><div></div>"
+      }
+    },
+    {
+      "data": "<dummy><template><span></dummy>",
+      "errors": [
+        "no doctype",
+        "bad end tag </dummy>",
+        "eof in template",
+        "eof in dummy"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "dummy": true,
+            "template": true,
+            "span": true
+          },
+          "template": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "dummy",
+                    "children": [
+                      {
+                        "tag": "template",
+                        "children": [
+                          {
+                            "content": true,
+                            "children": [
+                              {
+                                "tag": "span"
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><dummy><template><span></span></template></dummy></body></html>",
+        "noQuirksBodyHtml": "<dummy><template><span></span></template></dummy>"
+      }
+    },
+    {
+      "data": "<body><table><tr><td><select><template>Foo</template><caption>A</table>",
+      "errors": [
+        "no doctype",
+        "(1,62): unexpected-caption-in-select-in-table"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true,
+            "tbody": true,
+            "tr": true,
+            "td": true,
+            "select": true,
+            "template": true,
+            "caption": true
+          },
+          "template": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "tbody",
+                        "children": [
+                          {
+                            "tag": "tr",
+                            "children": [
+                              {
+                                "tag": "td",
+                                "children": [
+                                  {
+                                    "tag": "select",
+                                    "children": [
+                                      {
+                                        "tag": "template",
+                                        "children": [
+                                          {
+                                            "content": true,
+                                            "children": [
+                                              {
+                                                "text": "Foo"
+                                              }
+                                            ]
+                                          }
+                                        ]
+                                      }
+                                    ]
+                                  }
+                                ]
+                              }
+                            ]
+                          }
+                        ]
+                      },
+                      {
+                        "tag": "caption",
+                        "children": [
+                          {
+                            "text": "A"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><table><tbody><tr><td><select><template>Foo</template></select></td></tr></tbody><caption>A</caption></table></body></html>",
+        "noQuirksBodyHtml": "<table><tbody><tr><td><select><template>Foo</template></select></td></tr></tbody><caption>A</caption></table>"
+      }
+    },
+    {
+      "data": "<body></body><template>",
+      "errors": [
+        "no doctype",
+        "(1,23): template-after-body",
+        "(1,24): eof-in-template"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "template": true
+          },
+          "template": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "template",
+                    "children": [
+                      {
+                        "content": true
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><template></template></body></html>",
+        "noQuirksBodyHtml": "<template></template>"
+      }
+    },
+    {
+      "data": "<head></head><template>",
+      "errors": [
+        "no doctype",
+        "(1,23): template-after-head",
+        "(1,24): eof-in-template"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "template": true,
+            "body": true
+          },
+          "template": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "template",
+                    "children": [
+                      {
+                        "content": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><template></template></head><body></body></html>",
+        "noQuirksBodyHtml": "<template></template>"
+      }
+    },
+    {
+      "data": "<head></head><template>Foo</template>",
+      "errors": [
+        "no doctype",
+        "(1,23): template-after-head"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "template": true,
+            "body": true
+          },
+          "template": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "template",
+                    "children": [
+                      {
+                        "content": true,
+                        "children": [
+                          {
+                            "text": "Foo"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><template>Foo</template></head><body></body></html>",
+        "noQuirksBodyHtml": "<template>Foo</template>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE HTML><dummy><table><template><table><template><table><script>",
+      "errors": [
+        "eof script",
+        "eof template",
+        "eof template",
+        "eof table"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "dummy": true,
+            "table": true,
+            "template": true,
+            "script": true
+          },
+          "doctype": true,
+          "template": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "dummy",
+                    "children": [
+                      {
+                        "tag": "table",
+                        "children": [
+                          {
+                            "tag": "template",
+                            "children": [
+                              {
+                                "content": true,
+                                "children": [
+                                  {
+                                    "tag": "table",
+                                    "children": [
+                                      {
+                                        "tag": "template",
+                                        "children": [
+                                          {
+                                            "content": true,
+                                            "children": [
+                                              {
+                                                "tag": "table",
+                                                "children": [
+                                                  {
+                                                    "tag": "script"
+                                                  }
+                                                ]
+                                              }
+                                            ]
+                                          }
+                                        ]
+                                      }
+                                    ]
+                                  }
+                                ]
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><dummy><table><template><table><template><table><script></script></table></template></table></template></table></dummy></body></html>",
+        "noQuirksBodyHtml": "<dummy><table><template><table><template><table><script></script></table></template></table></template></table></dummy>"
+      }
+    },
+    {
+      "data": "<template><a><table><a>",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "template": true,
+            "a": true,
+            "table": true,
+            "body": true
+          },
+          "template": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "template",
+                    "children": [
+                      {
+                        "content": true,
+                        "children": [
+                          {
+                            "tag": "a",
+                            "children": [
+                              {
+                                "tag": "a"
+                              },
+                              {
+                                "tag": "table"
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><template><a><a></a><table></table></a></template></head><body></body></html>",
+        "noQuirksBodyHtml": "<template><a><a></a><table></table></a></template>"
+      }
+    }
+  ],
+  "tests1.dat": [
+    {
+      "data": "Test",
+      "errors": [
+        "(1,0): expected-doctype-but-got-chars"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "Test"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>Test</body></html>",
+        "noQuirksBodyHtml": "Test"
+      }
+    },
+    {
+      "data": "<p>One<p>Two",
+      "errors": [
+        "(1,3): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "p": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "p",
+                    "children": [
+                      {
+                        "text": "One"
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "p",
+                    "children": [
+                      {
+                        "text": "Two"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><p>One</p><p>Two</p></body></html>",
+        "noQuirksBodyHtml": "<p>One</p><p>Two</p>"
+      }
+    },
+    {
+      "data": "Line1<br>Line2<br>Line3<br>Line4",
+      "errors": [
+        "(1,0): expected-doctype-but-got-chars"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "br": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "Line1"
+                  },
+                  {
+                    "tag": "br"
+                  },
+                  {
+                    "text": "Line2"
+                  },
+                  {
+                    "tag": "br"
+                  },
+                  {
+                    "text": "Line3"
+                  },
+                  {
+                    "tag": "br"
+                  },
+                  {
+                    "text": "Line4"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>Line1<br>Line2<br>Line3<br>Line4</body></html>",
+        "noQuirksBodyHtml": "Line1<br>Line2<br>Line3<br>Line4"
+      }
+    },
+    {
+      "data": "<html>",
+      "errors": [
+        "(1,6): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body></body></html>",
+        "noQuirksBodyHtml": ""
+      }
+    },
+    {
+      "data": "<head>",
+      "errors": [
+        "(1,6): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body></body></html>",
+        "noQuirksBodyHtml": ""
+      }
+    },
+    {
+      "data": "<body>",
+      "errors": [
+        "(1,6): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body></body></html>",
+        "noQuirksBodyHtml": ""
+      }
+    },
+    {
+      "data": "<html><head>",
+      "errors": [
+        "(1,6): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body></body></html>",
+        "noQuirksBodyHtml": ""
+      }
+    },
+    {
+      "data": "<html><head></head>",
+      "errors": [
+        "(1,6): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body></body></html>",
+        "noQuirksBodyHtml": ""
+      }
+    },
+    {
+      "data": "<html><head></head><body>",
+      "errors": [
+        "(1,6): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body></body></html>",
+        "noQuirksBodyHtml": ""
+      }
+    },
+    {
+      "data": "<html><head></head><body></body>",
+      "errors": [
+        "(1,6): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body></body></html>",
+        "noQuirksBodyHtml": ""
+      }
+    },
+    {
+      "data": "<html><head><body></body></html>",
+      "errors": [
+        "(1,6): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body></body></html>",
+        "noQuirksBodyHtml": ""
+      }
+    },
+    {
+      "data": "<html><head></body></html>",
+      "errors": [
+        "(1,6): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body></body></html>",
+        "noQuirksBodyHtml": ""
+      }
+    },
+    {
+      "data": "<html><head><body></html>",
+      "errors": [
+        "(1,6): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body></body></html>",
+        "noQuirksBodyHtml": ""
+      }
+    },
+    {
+      "data": "<html><body></html>",
+      "errors": [
+        "(1,6): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body></body></html>",
+        "noQuirksBodyHtml": ""
+      }
+    },
+    {
+      "data": "<body></html>",
+      "errors": [
+        "(1,6): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body></body></html>",
+        "noQuirksBodyHtml": ""
+      }
+    },
+    {
+      "data": "<head></html>",
+      "errors": [
+        "(1,6): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body></body></html>",
+        "noQuirksBodyHtml": ""
+      }
+    },
+    {
+      "data": "</head>",
+      "errors": [
+        "(1,7): expected-doctype-but-got-end-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body></body></html>",
+        "noQuirksBodyHtml": ""
+      }
+    },
+    {
+      "data": "</body>",
+      "errors": [
+        "(1,7): expected-doctype-but-got-end-tag element."
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body></body></html>",
+        "noQuirksBodyHtml": ""
+      }
+    },
+    {
+      "data": "</html>",
+      "errors": [
+        "(1,7): expected-doctype-but-got-end-tag element."
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body></body></html>",
+        "noQuirksBodyHtml": ""
+      }
+    },
+    {
+      "data": "<b><table><td><i></table>",
+      "errors": [
+        "(1,3): expected-doctype-but-got-start-tag",
+        "(1,14): unexpected-cell-in-table-body",
+        "(1,25): unexpected-cell-end-tag",
+        "(1,25): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "b": true,
+            "table": true,
+            "tbody": true,
+            "tr": true,
+            "td": true,
+            "i": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "b",
+                    "children": [
+                      {
+                        "tag": "table",
+                        "children": [
+                          {
+                            "tag": "tbody",
+                            "children": [
+                              {
+                                "tag": "tr",
+                                "children": [
+                                  {
+                                    "tag": "td",
+                                    "children": [
+                                      {
+                                        "tag": "i"
+                                      }
+                                    ]
+                                  }
+                                ]
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><b><table><tbody><tr><td><i></i></td></tr></tbody></table></b></body></html>",
+        "noQuirksBodyHtml": "<b><table><tbody><tr><td><i></i></td></tr></tbody></table></b>"
+      }
+    },
+    {
+      "data": "<b><table><td></b><i></table>X",
+      "errors": [
+        "(1,3): expected-doctype-but-got-start-tag",
+        "(1,14): unexpected-cell-in-table-body",
+        "(1,18): unexpected-end-tag",
+        "(1,29): unexpected-cell-end-tag",
+        "(1,30): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "b": true,
+            "table": true,
+            "tbody": true,
+            "tr": true,
+            "td": true,
+            "i": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "b",
+                    "children": [
+                      {
+                        "tag": "table",
+                        "children": [
+                          {
+                            "tag": "tbody",
+                            "children": [
+                              {
+                                "tag": "tr",
+                                "children": [
+                                  {
+                                    "tag": "td",
+                                    "children": [
+                                      {
+                                        "tag": "i"
+                                      }
+                                    ]
+                                  }
+                                ]
+                              }
+                            ]
+                          }
+                        ]
+                      },
+                      {
+                        "text": "X"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><b><table><tbody><tr><td><i></i></td></tr></tbody></table>X</b></body></html>",
+        "noQuirksBodyHtml": "<b><table><tbody><tr><td><i></i></td></tr></tbody></table>X</b>"
+      }
+    },
+    {
+      "data": "<h1>Hello<h2>World",
+      "errors": [
+        "(1,4): expected-doctype-but-got-start-tag",
+        "(1,13): unexpected-start-tag",
+        "(1,18): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "h1": true,
+            "h2": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "h1",
+                    "children": [
+                      {
+                        "text": "Hello"
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "h2",
+                    "children": [
+                      {
+                        "text": "World"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><h1>Hello</h1><h2>World</h2></body></html>",
+        "noQuirksBodyHtml": "<h1>Hello</h1><h2>World</h2>"
+      }
+    },
+    {
+      "data": "<a><p>X<a>Y</a>Z</p></a>",
+      "errors": [
+        "(1,3): expected-doctype-but-got-start-tag",
+        "(1,10): unexpected-start-tag-implies-end-tag",
+        "(1,10): adoption-agency-1.3",
+        "(1,24): unexpected-end-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "a": true,
+            "p": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "a"
+                  },
+                  {
+                    "tag": "p",
+                    "children": [
+                      {
+                        "tag": "a",
+                        "children": [
+                          {
+                            "text": "X"
+                          }
+                        ]
+                      },
+                      {
+                        "tag": "a",
+                        "children": [
+                          {
+                            "text": "Y"
+                          }
+                        ]
+                      },
+                      {
+                        "text": "Z"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><a></a><p><a>X</a><a>Y</a>Z</p></body></html>",
+        "noQuirksBodyHtml": "<a></a><p><a>X</a><a>Y</a>Z</p>"
+      }
+    },
+    {
+      "data": "<b><button>foo</b>bar",
+      "errors": [
+        "(1,3): expected-doctype-but-got-start-tag",
+        "(1,18): adoption-agency-1.3",
+        "(1,21): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "b": true,
+            "button": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "b"
+                  },
+                  {
+                    "tag": "button",
+                    "children": [
+                      {
+                        "tag": "b",
+                        "children": [
+                          {
+                            "text": "foo"
+                          }
+                        ]
+                      },
+                      {
+                        "text": "bar"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><b></b><button><b>foo</b>bar</button></body></html>",
+        "noQuirksBodyHtml": "<b></b><button><b>foo</b>bar</button>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><span><button>foo</span>bar",
+      "errors": [
+        "(1,39): unexpected-end-tag",
+        "(1,42): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "span": true,
+            "button": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "span",
+                    "children": [
+                      {
+                        "tag": "button",
+                        "children": [
+                          {
+                            "text": "foobar"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><span><button>foobar</button></span></body></html>",
+        "noQuirksBodyHtml": "<span><button>foobar</button></span>"
+      }
+    },
+    {
+      "data": "<p><b><div><marquee></p></b></div>X",
+      "errors": [
+        "(1,3): expected-doctype-but-got-start-tag",
+        "(1,11): unexpected-end-tag",
+        "(1,24): unexpected-end-tag",
+        "(1,28): unexpected-end-tag",
+        "(1,34): end-tag-too-early",
+        "(1,35): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "p": true,
+            "b": true,
+            "div": true,
+            "marquee": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "p",
+                    "children": [
+                      {
+                        "tag": "b"
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "div",
+                    "children": [
+                      {
+                        "tag": "b",
+                        "children": [
+                          {
+                            "tag": "marquee",
+                            "children": [
+                              {
+                                "tag": "p"
+                              },
+                              {
+                                "text": "X"
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><p><b></b></p><div><b><marquee><p></p>X</marquee></b></div></body></html>",
+        "noQuirksBodyHtml": "<p><b></b></p><div><b><marquee><p></p>X</marquee></b></div>"
+      }
+    },
+    {
+      "data": "<script><div></script></div><title><p></title><p><p>",
+      "errors": [
+        "(1,8): expected-doctype-but-got-start-tag",
+        "(1,28): unexpected-end-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "title": true,
+            "body": true,
+            "p": true
+          },
+          "no_escape": true,
+          "escaped": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "<div>",
+                        "no_escape": true
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "title",
+                    "children": [
+                      {
+                        "text": "<p>",
+                        "escaped": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "p"
+                  },
+                  {
+                    "tag": "p"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><script><div></script><title>&lt;p&gt;</title></head><body><p></p><p></p></body></html>",
+        "noQuirksBodyHtml": "<script><div></script><title>&lt;p&gt;</title><p></p><p></p>"
+      }
+    },
+    {
+      "data": "<!--><div>--<!-->",
+      "errors": [
+        "(1,5): incorrect-comment",
+        "(1,10): expected-doctype-but-got-start-tag",
+        "(1,17): incorrect-comment",
+        "(1,17): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "div": true
+          },
+          "comment": true
+        },
+        "tree": [
+          {
+            "comment": ""
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "div",
+                    "children": [
+                      {
+                        "text": "--"
+                      },
+                      {
+                        "comment": ""
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!----><html><head></head><body><div>--<!----></div></body></html>",
+        "noQuirksBodyHtml": "<!----><div>--<!----></div>"
+      }
+    },
+    {
+      "data": "<p><hr></p>",
+      "errors": [
+        "(1,3): expected-doctype-but-got-start-tag",
+        "(1,11): unexpected-end-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "p": true,
+            "hr": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "p"
+                  },
+                  {
+                    "tag": "hr"
+                  },
+                  {
+                    "tag": "p"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><p></p><hr><p></p></body></html>",
+        "noQuirksBodyHtml": "<p></p><hr><p></p>"
+      }
+    },
+    {
+      "data": "<select><b><option><select><option></b></select>X",
+      "errors": [
+        "(1,8): expected-doctype-but-got-start-tag",
+        "(1,11): unexpected-start-tag-in-select",
+        "(1,27): unexpected-select-in-select",
+        "(1,39): unexpected-end-tag",
+        "(1,48): unexpected-end-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "select": true,
+            "option": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "select",
+                    "children": [
+                      {
+                        "tag": "option"
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "option",
+                    "children": [
+                      {
+                        "text": "X"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><select><option></option></select><option>X</option></body></html>",
+        "noQuirksBodyHtml": "<select><option></option></select><option>X</option>"
+      }
+    },
+    {
+      "data": "<a><table><td><a><table></table><a></tr><a></table><b>X</b>C<a>Y",
+      "errors": [
+        "(1,3): expected-doctype-but-got-start-tag",
+        "(1,14): unexpected-cell-in-table-body",
+        "(1,35): unexpected-start-tag-implies-end-tag",
+        "(1,40): unexpected-cell-end-tag",
+        "(1,43): unexpected-start-tag-implies-table-voodoo",
+        "(1,43): unexpected-start-tag-implies-end-tag",
+        "(1,43): unexpected-end-tag",
+        "(1,63): unexpected-start-tag-implies-end-tag",
+        "(1,64): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "a": true,
+            "table": true,
+            "tbody": true,
+            "tr": true,
+            "td": true,
+            "b": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "a",
+                    "children": [
+                      {
+                        "tag": "a"
+                      },
+                      {
+                        "tag": "table",
+                        "children": [
+                          {
+                            "tag": "tbody",
+                            "children": [
+                              {
+                                "tag": "tr",
+                                "children": [
+                                  {
+                                    "tag": "td",
+                                    "children": [
+                                      {
+                                        "tag": "a",
+                                        "children": [
+                                          {
+                                            "tag": "table"
+                                          }
+                                        ]
+                                      },
+                                      {
+                                        "tag": "a"
+                                      }
+                                    ]
+                                  }
+                                ]
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "a",
+                    "children": [
+                      {
+                        "tag": "b",
+                        "children": [
+                          {
+                            "text": "X"
+                          }
+                        ]
+                      },
+                      {
+                        "text": "C"
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "a",
+                    "children": [
+                      {
+                        "text": "Y"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><a><a></a><table><tbody><tr><td><a><table></table></a><a></a></td></tr></tbody></table></a><a><b>X</b>C</a><a>Y</a></body></html>",
+        "noQuirksBodyHtml": "<a><a></a><table><tbody><tr><td><a><table></table></a><a></a></td></tr></tbody></table></a><a><b>X</b>C</a><a>Y</a>"
+      }
+    },
+    {
+      "data": "<a X>0<b>1<a Y>2",
+      "errors": [
+        "(1,5): expected-doctype-but-got-start-tag",
+        "(1,15): unexpected-start-tag-implies-end-tag",
+        "(1,15): adoption-agency-1.3",
+        "(1,16): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "a": true,
+            "b": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "a",
+                    "attrs": [
+                      {
+                        "name": "x",
+                        "value": ""
+                      }
+                    ],
+                    "children": [
+                      {
+                        "text": "0"
+                      },
+                      {
+                        "tag": "b",
+                        "children": [
+                          {
+                            "text": "1"
+                          }
+                        ]
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "b",
+                    "children": [
+                      {
+                        "tag": "a",
+                        "attrs": [
+                          {
+                            "name": "y",
+                            "value": ""
+                          }
+                        ],
+                        "children": [
+                          {
+                            "text": "2"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><a x=\"\">0<b>1</b></a><b><a y=\"\">2</a></b></body></html>",
+        "noQuirksBodyHtml": "<a x=\"\">0<b>1</b></a><b><a y=\"\">2</a></b>"
+      }
+    },
+    {
+      "data": "<!-----><font><div>hello<table>excite!<b>me!<th><i>please!</tr><!--X-->",
+      "errors": [
+        "(1,7): unexpected-dash-after-double-dash-in-comment",
+        "(1,14): expected-doctype-but-got-start-tag",
+        "(1,41): unexpected-start-tag-implies-table-voodoo",
+        "(1,48): foster-parenting-character-in-table",
+        "(1,48): foster-parenting-character-in-table",
+        "(1,48): foster-parenting-character-in-table",
+        "(1,48): foster-parenting-character-in-table",
+        "(1,48): foster-parenting-character-in-table",
+        "(1,48): foster-parenting-character-in-table",
+        "(1,48): foster-parenting-character-in-table",
+        "(1,48): foster-parenting-character-in-table",
+        "(1,48): foster-parenting-character-in-table",
+        "(1,48): foster-parenting-character-in-table",
+        "(1,48): unexpected-cell-in-table-body",
+        "(1,63): unexpected-cell-end-tag",
+        "(1,71): eof-in-table"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "font": true,
+            "div": true,
+            "b": true,
+            "table": true,
+            "tbody": true,
+            "tr": true,
+            "th": true,
+            "i": true
+          },
+          "comment": true
+        },
+        "tree": [
+          {
+            "comment": "-"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "font",
+                    "children": [
+                      {
+                        "tag": "div",
+                        "children": [
+                          {
+                            "text": "helloexcite!"
+                          },
+                          {
+                            "tag": "b",
+                            "children": [
+                              {
+                                "text": "me!"
+                              }
+                            ]
+                          },
+                          {
+                            "tag": "table",
+                            "children": [
+                              {
+                                "tag": "tbody",
+                                "children": [
+                                  {
+                                    "tag": "tr",
+                                    "children": [
+                                      {
+                                        "tag": "th",
+                                        "children": [
+                                          {
+                                            "tag": "i",
+                                            "children": [
+                                              {
+                                                "text": "please!"
+                                              }
+                                            ]
+                                          }
+                                        ]
+                                      }
+                                    ]
+                                  },
+                                  {
+                                    "comment": "X"
+                                  }
+                                ]
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!-----><html><head></head><body><font><div>helloexcite!<b>me!</b><table><tbody><tr><th><i>please!</i></th></tr><!--X--></tbody></table></div></font></body></html>",
+        "noQuirksBodyHtml": "<!-----><font><div>helloexcite!<b>me!</b><table><tbody><tr><th><i>please!</i></th></tr><!--X--></tbody></table></div></font>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><li>hello<li>world<ul>how<li>do</ul>you</body><!--do-->",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "li": true,
+            "ul": true
+          },
+          "doctype": true,
+          "comment": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "li",
+                    "children": [
+                      {
+                        "text": "hello"
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "li",
+                    "children": [
+                      {
+                        "text": "world"
+                      },
+                      {
+                        "tag": "ul",
+                        "children": [
+                          {
+                            "text": "how"
+                          },
+                          {
+                            "tag": "li",
+                            "children": [
+                              {
+                                "text": "do"
+                              }
+                            ]
+                          }
+                        ]
+                      },
+                      {
+                        "text": "you"
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "comment": "do"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><li>hello</li><li>world<ul>how<li>do</li></ul>you</li></body><!--do--></html>",
+        "noQuirksBodyHtml": "<li>hello</li><li>world<ul>how<li>do</li></ul>you<!--do--></li>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html>A<option>B<optgroup>C<select>D</option>E",
+      "errors": [
+        "(1,54): unexpected-end-tag-in-select",
+        "(1,55): eof-in-select"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "option": true,
+            "optgroup": true,
+            "select": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "A"
+                  },
+                  {
+                    "tag": "option",
+                    "children": [
+                      {
+                        "text": "B"
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "optgroup",
+                    "children": [
+                      {
+                        "text": "C"
+                      },
+                      {
+                        "tag": "select",
+                        "children": [
+                          {
+                            "text": "DE"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body>A<option>B</option><optgroup>C<select>DE</select></optgroup></body></html>",
+        "noQuirksBodyHtml": "A<option>B</option><optgroup>C<select>DE</select></optgroup>"
+      }
+    },
+    {
+      "data": "<",
+      "errors": [
+        "(1,1): expected-tag-name",
+        "(1,1): expected-doctype-but-got-chars"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "escaped": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "<",
+                    "escaped": true
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>&lt;</body></html>",
+        "noQuirksBodyHtml": "&lt;"
+      }
+    },
+    {
+      "data": "<#",
+      "errors": [
+        "(1,1): expected-tag-name",
+        "(1,1): expected-doctype-but-got-chars"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "escaped": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "<#",
+                    "escaped": true
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>&lt;#</body></html>",
+        "noQuirksBodyHtml": "&lt;#"
+      }
+    },
+    {
+      "data": "</",
+      "errors": [
+        "(1,2): expected-closing-tag-but-got-eof",
+        "(1,2): expected-doctype-but-got-chars"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "escaped": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "</",
+                    "escaped": true
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>&lt;/</body></html>",
+        "noQuirksBodyHtml": "&lt;/"
+      }
+    },
+    {
+      "data": "</#",
+      "errors": [
+        "(1,2): expected-closing-tag-but-got-char",
+        "(1,3): expected-doctype-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "comment": true
+        },
+        "tree": [
+          {
+            "comment": "#"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!--#--><html><head></head><body></body></html>",
+        "noQuirksBodyHtml": "<!--#-->"
+      }
+    },
+    {
+      "data": "<?",
+      "errors": [
+        "(1,1): expected-tag-name-but-got-question-mark",
+        "(1,2): expected-doctype-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "comment": true
+        },
+        "tree": [
+          {
+            "comment": "?"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!--?--><html><head></head><body></body></html>",
+        "noQuirksBodyHtml": "<!--?-->"
+      }
+    },
+    {
+      "data": "<?#",
+      "errors": [
+        "(1,1): expected-tag-name-but-got-question-mark",
+        "(1,3): expected-doctype-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "comment": true
+        },
+        "tree": [
+          {
+            "comment": "?#"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!--?#--><html><head></head><body></body></html>",
+        "noQuirksBodyHtml": "<!--?#-->"
+      }
+    },
+    {
+      "data": "<!",
+      "errors": [
+        "(1,2): expected-dashes-or-doctype",
+        "(1,2): expected-doctype-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "comment": true
+        },
+        "tree": [
+          {
+            "comment": ""
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!----><html><head></head><body></body></html>",
+        "noQuirksBodyHtml": "<!---->"
+      }
+    },
+    {
+      "data": "<!#",
+      "errors": [
+        "(1,2): expected-dashes-or-doctype",
+        "(1,3): expected-doctype-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "comment": true
+        },
+        "tree": [
+          {
+            "comment": "#"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!--#--><html><head></head><body></body></html>",
+        "noQuirksBodyHtml": "<!--#-->"
+      }
+    },
+    {
+      "data": "<?COMMENT?>",
+      "errors": [
+        "(1,1): expected-tag-name-but-got-question-mark",
+        "(1,11): expected-doctype-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "comment": true
+        },
+        "tree": [
+          {
+            "comment": "?COMMENT?"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!--?COMMENT?--><html><head></head><body></body></html>",
+        "noQuirksBodyHtml": "<!--?COMMENT?-->"
+      }
+    },
+    {
+      "data": "<!COMMENT>",
+      "errors": [
+        "(1,2): expected-dashes-or-doctype",
+        "(1,10): expected-doctype-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "comment": true
+        },
+        "tree": [
+          {
+            "comment": "COMMENT"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!--COMMENT--><html><head></head><body></body></html>",
+        "noQuirksBodyHtml": "<!--COMMENT-->"
+      }
+    },
+    {
+      "data": "</ COMMENT >",
+      "errors": [
+        "(1,2): expected-closing-tag-but-got-char",
+        "(1,12): expected-doctype-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "comment": true
+        },
+        "tree": [
+          {
+            "comment": " COMMENT "
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!-- COMMENT --><html><head></head><body></body></html>",
+        "noQuirksBodyHtml": "<!-- COMMENT -->"
+      }
+    },
+    {
+      "data": "<?COM--MENT?>",
+      "errors": [
+        "(1,1): expected-tag-name-but-got-question-mark",
+        "(1,13): expected-doctype-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "comment": true
+        },
+        "tree": [
+          {
+            "comment": "?COM--MENT?"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!--?COM--MENT?--><html><head></head><body></body></html>",
+        "noQuirksBodyHtml": "<!--?COM--MENT?-->"
+      }
+    },
+    {
+      "data": "<!COM--MENT>",
+      "errors": [
+        "(1,2): expected-dashes-or-doctype",
+        "(1,12): expected-doctype-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "comment": true
+        },
+        "tree": [
+          {
+            "comment": "COM--MENT"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!--COM--MENT--><html><head></head><body></body></html>",
+        "noQuirksBodyHtml": "<!--COM--MENT-->"
+      }
+    },
+    {
+      "data": "</ COM--MENT >",
+      "errors": [
+        "(1,2): expected-closing-tag-but-got-char",
+        "(1,14): expected-doctype-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "comment": true
+        },
+        "tree": [
+          {
+            "comment": " COM--MENT "
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!-- COM--MENT --><html><head></head><body></body></html>",
+        "noQuirksBodyHtml": "<!-- COM--MENT -->"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><style> EOF",
+      "errors": [
+        "(1,26): expected-named-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "style": true,
+            "body": true
+          },
+          "doctype": true,
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "style",
+                    "children": [
+                      {
+                        "text": " EOF",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head><style> EOF</style></head><body></body></html>",
+        "noQuirksBodyHtml": "<style> EOF</style>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><script> <!-- </script> --> </script> EOF",
+      "errors": [
+        "(1,52): unexpected-end-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "doctype": true,
+          "no_escape": true,
+          "escaped": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": " <!-- ",
+                        "no_escape": true
+                      }
+                    ]
+                  },
+                  {
+                    "text": " "
+                  }
+                ]
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "-->  EOF",
+                    "escaped": true
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head><script> <!-- </script> </head><body>--&gt;  EOF</body></html>",
+        "noQuirksBodyHtml": "<script> <!-- </script> --&gt;  EOF"
+      }
+    },
+    {
+      "data": "<b><p></b>TEST",
+      "errors": [
+        "(1,3): expected-doctype-but-got-start-tag",
+        "(1,10): adoption-agency-1.3"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "b": true,
+            "p": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "b"
+                  },
+                  {
+                    "tag": "p",
+                    "children": [
+                      {
+                        "tag": "b"
+                      },
+                      {
+                        "text": "TEST"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><b></b><p><b></b>TEST</p></body></html>",
+        "noQuirksBodyHtml": "<b></b><p><b></b>TEST</p>"
+      }
+    },
+    {
+      "data": "<p id=a><b><p id=b></b>TEST",
+      "errors": [
+        "(1,8): expected-doctype-but-got-start-tag",
+        "(1,19): unexpected-end-tag",
+        "(1,23): adoption-agency-1.2"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "p": true,
+            "b": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "p",
+                    "attrs": [
+                      {
+                        "name": "id",
+                        "value": "a"
+                      }
+                    ],
+                    "children": [
+                      {
+                        "tag": "b"
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "p",
+                    "attrs": [
+                      {
+                        "name": "id",
+                        "value": "b"
+                      }
+                    ],
+                    "children": [
+                      {
+                        "text": "TEST"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><p id=\"a\"><b></b></p><p id=\"b\">TEST</p></body></html>",
+        "noQuirksBodyHtml": "<p id=\"a\"><b></b></p><p id=\"b\">TEST</p>"
+      }
+    },
+    {
+      "data": "<b id=a><p><b id=b></p></b>TEST",
+      "errors": [
+        "(1,8): expected-doctype-but-got-start-tag",
+        "(1,23): unexpected-end-tag",
+        "(1,27): adoption-agency-1.2",
+        "(1,31): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "b": true,
+            "p": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "b",
+                    "attrs": [
+                      {
+                        "name": "id",
+                        "value": "a"
+                      }
+                    ],
+                    "children": [
+                      {
+                        "tag": "p",
+                        "children": [
+                          {
+                            "tag": "b",
+                            "attrs": [
+                              {
+                                "name": "id",
+                                "value": "b"
+                              }
+                            ]
+                          }
+                        ]
+                      },
+                      {
+                        "text": "TEST"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><b id=\"a\"><p><b id=\"b\"></b></p>TEST</b></body></html>",
+        "noQuirksBodyHtml": "<b id=\"a\"><p><b id=\"b\"></b></p>TEST</b>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><title>U-test</title><body><div><p>Test<u></p></div></body>",
+      "errors": [
+        "(1,61): unexpected-end-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "title": true,
+            "body": true,
+            "div": true,
+            "p": true,
+            "u": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "title",
+                    "children": [
+                      {
+                        "text": "U-test"
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "div",
+                    "children": [
+                      {
+                        "tag": "p",
+                        "children": [
+                          {
+                            "text": "Test"
+                          },
+                          {
+                            "tag": "u"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head><title>U-test</title></head><body><div><p>Test<u></u></p></div></body></html>",
+        "noQuirksBodyHtml": "<title>U-test</title><div><p>Test<u></u></p></div>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><font><table></font></table></font>",
+      "errors": [
+        "(1,35): unexpected-end-tag-implies-table-voodoo",
+        "(1,35): unexpected-end-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "font": true,
+            "table": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "font",
+                    "children": [
+                      {
+                        "tag": "table"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><font><table></table></font></body></html>",
+        "noQuirksBodyHtml": "<font><table></table></font>"
+      }
+    },
+    {
+      "data": "<font><p>hello<b>cruel</font>world",
+      "errors": [
+        "(1,6): expected-doctype-but-got-start-tag",
+        "(1,29): adoption-agency-1.3",
+        "(1,29): adoption-agency-1.3",
+        "(1,34): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "font": true,
+            "p": true,
+            "b": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "font"
+                  },
+                  {
+                    "tag": "p",
+                    "children": [
+                      {
+                        "tag": "font",
+                        "children": [
+                          {
+                            "text": "hello"
+                          },
+                          {
+                            "tag": "b",
+                            "children": [
+                              {
+                                "text": "cruel"
+                              }
+                            ]
+                          }
+                        ]
+                      },
+                      {
+                        "tag": "b",
+                        "children": [
+                          {
+                            "text": "world"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><font></font><p><font>hello<b>cruel</b></font><b>world</b></p></body></html>",
+        "noQuirksBodyHtml": "<font></font><p><font>hello<b>cruel</b></font><b>world</b></p>"
+      }
+    },
+    {
+      "data": "<b>Test</i>Test",
+      "errors": [
+        "(1,3): expected-doctype-but-got-start-tag",
+        "(1,11): unexpected-end-tag",
+        "(1,15): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "b": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "b",
+                    "children": [
+                      {
+                        "text": "TestTest"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><b>TestTest</b></body></html>",
+        "noQuirksBodyHtml": "<b>TestTest</b>"
+      }
+    },
+    {
+      "data": "<b>A<cite>B<div>C",
+      "errors": [
+        "(1,3): expected-doctype-but-got-start-tag",
+        "(1,17): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "b": true,
+            "cite": true,
+            "div": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "b",
+                    "children": [
+                      {
+                        "text": "A"
+                      },
+                      {
+                        "tag": "cite",
+                        "children": [
+                          {
+                            "text": "B"
+                          },
+                          {
+                            "tag": "div",
+                            "children": [
+                              {
+                                "text": "C"
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><b>A<cite>B<div>C</div></cite></b></body></html>",
+        "noQuirksBodyHtml": "<b>A<cite>B<div>C</div></cite></b>"
+      }
+    },
+    {
+      "data": "<b>A<cite>B<div>C</cite>D",
+      "errors": [
+        "(1,3): expected-doctype-but-got-start-tag",
+        "(1,24): unexpected-end-tag",
+        "(1,25): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "b": true,
+            "cite": true,
+            "div": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "b",
+                    "children": [
+                      {
+                        "text": "A"
+                      },
+                      {
+                        "tag": "cite",
+                        "children": [
+                          {
+                            "text": "B"
+                          },
+                          {
+                            "tag": "div",
+                            "children": [
+                              {
+                                "text": "CD"
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><b>A<cite>B<div>CD</div></cite></b></body></html>",
+        "noQuirksBodyHtml": "<b>A<cite>B<div>CD</div></cite></b>"
+      }
+    },
+    {
+      "data": "<b>A<cite>B<div>C</b>D",
+      "errors": [
+        "(1,3): expected-doctype-but-got-start-tag",
+        "(1,21): adoption-agency-1.3",
+        "(1,22): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "b": true,
+            "cite": true,
+            "div": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "b",
+                    "children": [
+                      {
+                        "text": "A"
+                      },
+                      {
+                        "tag": "cite",
+                        "children": [
+                          {
+                            "text": "B"
+                          }
+                        ]
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "div",
+                    "children": [
+                      {
+                        "tag": "b",
+                        "children": [
+                          {
+                            "text": "C"
+                          }
+                        ]
+                      },
+                      {
+                        "text": "D"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><b>A<cite>B</cite></b><div><b>C</b>D</div></body></html>",
+        "noQuirksBodyHtml": "<b>A<cite>B</cite></b><div><b>C</b>D</div>"
+      }
+    },
+    {
+      "data": "",
+      "errors": [
+        "(1,0): expected-doctype-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body></body></html>",
+        "noQuirksBodyHtml": ""
+      }
+    },
+    {
+      "data": "<DIV>",
+      "errors": [
+        "(1,5): expected-doctype-but-got-start-tag",
+        "(1,5): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "div": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "div"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><div></div></body></html>",
+        "noQuirksBodyHtml": "<div></div>"
+      }
+    },
+    {
+      "data": "<DIV> abc",
+      "errors": [
+        "(1,5): expected-doctype-but-got-start-tag",
+        "(1,9): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "div": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "div",
+                    "children": [
+                      {
+                        "text": " abc"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><div> abc</div></body></html>",
+        "noQuirksBodyHtml": "<div> abc</div>"
+      }
+    },
+    {
+      "data": "<DIV> abc <B>",
+      "errors": [
+        "(1,5): expected-doctype-but-got-start-tag",
+        "(1,13): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "div": true,
+            "b": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "div",
+                    "children": [
+                      {
+                        "text": " abc "
+                      },
+                      {
+                        "tag": "b"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><div> abc <b></b></div></body></html>",
+        "noQuirksBodyHtml": "<div> abc <b></b></div>"
+      }
+    },
+    {
+      "data": "<DIV> abc <B> def",
+      "errors": [
+        "(1,5): expected-doctype-but-got-start-tag",
+        "(1,17): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "div": true,
+            "b": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "div",
+                    "children": [
+                      {
+                        "text": " abc "
+                      },
+                      {
+                        "tag": "b",
+                        "children": [
+                          {
+                            "text": " def"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><div> abc <b> def</b></div></body></html>",
+        "noQuirksBodyHtml": "<div> abc <b> def</b></div>"
+      }
+    },
+    {
+      "data": "<DIV> abc <B> def <I>",
+      "errors": [
+        "(1,5): expected-doctype-but-got-start-tag",
+        "(1,21): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "div": true,
+            "b": true,
+            "i": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "div",
+                    "children": [
+                      {
+                        "text": " abc "
+                      },
+                      {
+                        "tag": "b",
+                        "children": [
+                          {
+                            "text": " def "
+                          },
+                          {
+                            "tag": "i"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><div> abc <b> def <i></i></b></div></body></html>",
+        "noQuirksBodyHtml": "<div> abc <b> def <i></i></b></div>"
+      }
+    },
+    {
+      "data": "<DIV> abc <B> def <I> ghi",
+      "errors": [
+        "(1,5): expected-doctype-but-got-start-tag",
+        "(1,25): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "div": true,
+            "b": true,
+            "i": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "div",
+                    "children": [
+                      {
+                        "text": " abc "
+                      },
+                      {
+                        "tag": "b",
+                        "children": [
+                          {
+                            "text": " def "
+                          },
+                          {
+                            "tag": "i",
+                            "children": [
+                              {
+                                "text": " ghi"
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><div> abc <b> def <i> ghi</i></b></div></body></html>",
+        "noQuirksBodyHtml": "<div> abc <b> def <i> ghi</i></b></div>"
+      }
+    },
+    {
+      "data": "<DIV> abc <B> def <I> ghi <P>",
+      "errors": [
+        "(1,5): expected-doctype-but-got-start-tag",
+        "(1,29): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "div": true,
+            "b": true,
+            "i": true,
+            "p": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "div",
+                    "children": [
+                      {
+                        "text": " abc "
+                      },
+                      {
+                        "tag": "b",
+                        "children": [
+                          {
+                            "text": " def "
+                          },
+                          {
+                            "tag": "i",
+                            "children": [
+                              {
+                                "text": " ghi "
+                              },
+                              {
+                                "tag": "p"
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><div> abc <b> def <i> ghi <p></p></i></b></div></body></html>",
+        "noQuirksBodyHtml": "<div> abc <b> def <i> ghi <p></p></i></b></div>"
+      }
+    },
+    {
+      "data": "<DIV> abc <B> def <I> ghi <P> jkl",
+      "errors": [
+        "(1,5): expected-doctype-but-got-start-tag",
+        "(1,33): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "div": true,
+            "b": true,
+            "i": true,
+            "p": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "div",
+                    "children": [
+                      {
+                        "text": " abc "
+                      },
+                      {
+                        "tag": "b",
+                        "children": [
+                          {
+                            "text": " def "
+                          },
+                          {
+                            "tag": "i",
+                            "children": [
+                              {
+                                "text": " ghi "
+                              },
+                              {
+                                "tag": "p",
+                                "children": [
+                                  {
+                                    "text": " jkl"
+                                  }
+                                ]
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><div> abc <b> def <i> ghi <p> jkl</p></i></b></div></body></html>",
+        "noQuirksBodyHtml": "<div> abc <b> def <i> ghi <p> jkl</p></i></b></div>"
+      }
+    },
+    {
+      "data": "<DIV> abc <B> def <I> ghi <P> jkl </B>",
+      "errors": [
+        "(1,5): expected-doctype-but-got-start-tag",
+        "(1,38): adoption-agency-1.3",
+        "(1,38): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "div": true,
+            "b": true,
+            "i": true,
+            "p": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "div",
+                    "children": [
+                      {
+                        "text": " abc "
+                      },
+                      {
+                        "tag": "b",
+                        "children": [
+                          {
+                            "text": " def "
+                          },
+                          {
+                            "tag": "i",
+                            "children": [
+                              {
+                                "text": " ghi "
+                              }
+                            ]
+                          }
+                        ]
+                      },
+                      {
+                        "tag": "i",
+                        "children": [
+                          {
+                            "tag": "p",
+                            "children": [
+                              {
+                                "tag": "b",
+                                "children": [
+                                  {
+                                    "text": " jkl "
+                                  }
+                                ]
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><div> abc <b> def <i> ghi </i></b><i><p><b> jkl </b></p></i></div></body></html>",
+        "noQuirksBodyHtml": "<div> abc <b> def <i> ghi </i></b><i><p><b> jkl </b></p></i></div>"
+      }
+    },
+    {
+      "data": "<DIV> abc <B> def <I> ghi <P> jkl </B> mno",
+      "errors": [
+        "(1,5): expected-doctype-but-got-start-tag",
+        "(1,38): adoption-agency-1.3",
+        "(1,42): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "div": true,
+            "b": true,
+            "i": true,
+            "p": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "div",
+                    "children": [
+                      {
+                        "text": " abc "
+                      },
+                      {
+                        "tag": "b",
+                        "children": [
+                          {
+                            "text": " def "
+                          },
+                          {
+                            "tag": "i",
+                            "children": [
+                              {
+                                "text": " ghi "
+                              }
+                            ]
+                          }
+                        ]
+                      },
+                      {
+                        "tag": "i",
+                        "children": [
+                          {
+                            "tag": "p",
+                            "children": [
+                              {
+                                "tag": "b",
+                                "children": [
+                                  {
+                                    "text": " jkl "
+                                  }
+                                ]
+                              },
+                              {
+                                "text": " mno"
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><div> abc <b> def <i> ghi </i></b><i><p><b> jkl </b> mno</p></i></div></body></html>",
+        "noQuirksBodyHtml": "<div> abc <b> def <i> ghi </i></b><i><p><b> jkl </b> mno</p></i></div>"
+      }
+    },
+    {
+      "data": "<DIV> abc <B> def <I> ghi <P> jkl </B> mno </I>",
+      "errors": [
+        "(1,5): expected-doctype-but-got-start-tag",
+        "(1,38): adoption-agency-1.3",
+        "(1,47): adoption-agency-1.3",
+        "(1,47): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "div": true,
+            "b": true,
+            "i": true,
+            "p": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "div",
+                    "children": [
+                      {
+                        "text": " abc "
+                      },
+                      {
+                        "tag": "b",
+                        "children": [
+                          {
+                            "text": " def "
+                          },
+                          {
+                            "tag": "i",
+                            "children": [
+                              {
+                                "text": " ghi "
+                              }
+                            ]
+                          }
+                        ]
+                      },
+                      {
+                        "tag": "i"
+                      },
+                      {
+                        "tag": "p",
+                        "children": [
+                          {
+                            "tag": "i",
+                            "children": [
+                              {
+                                "tag": "b",
+                                "children": [
+                                  {
+                                    "text": " jkl "
+                                  }
+                                ]
+                              },
+                              {
+                                "text": " mno "
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><div> abc <b> def <i> ghi </i></b><i></i><p><i><b> jkl </b> mno </i></p></div></body></html>",
+        "noQuirksBodyHtml": "<div> abc <b> def <i> ghi </i></b><i></i><p><i><b> jkl </b> mno </i></p></div>"
+      }
+    },
+    {
+      "data": "<DIV> abc <B> def <I> ghi <P> jkl </B> mno </I> pqr",
+      "errors": [
+        "(1,5): expected-doctype-but-got-start-tag",
+        "(1,38): adoption-agency-1.3",
+        "(1,47): adoption-agency-1.3",
+        "(1,51): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "div": true,
+            "b": true,
+            "i": true,
+            "p": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "div",
+                    "children": [
+                      {
+                        "text": " abc "
+                      },
+                      {
+                        "tag": "b",
+                        "children": [
+                          {
+                            "text": " def "
+                          },
+                          {
+                            "tag": "i",
+                            "children": [
+                              {
+                                "text": " ghi "
+                              }
+                            ]
+                          }
+                        ]
+                      },
+                      {
+                        "tag": "i"
+                      },
+                      {
+                        "tag": "p",
+                        "children": [
+                          {
+                            "tag": "i",
+                            "children": [
+                              {
+                                "tag": "b",
+                                "children": [
+                                  {
+                                    "text": " jkl "
+                                  }
+                                ]
+                              },
+                              {
+                                "text": " mno "
+                              }
+                            ]
+                          },
+                          {
+                            "text": " pqr"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><div> abc <b> def <i> ghi </i></b><i></i><p><i><b> jkl </b> mno </i> pqr</p></div></body></html>",
+        "noQuirksBodyHtml": "<div> abc <b> def <i> ghi </i></b><i></i><p><i><b> jkl </b> mno </i> pqr</p></div>"
+      }
+    },
+    {
+      "data": "<DIV> abc <B> def <I> ghi <P> jkl </B> mno </I> pqr </P>",
+      "errors": [
+        "(1,5): expected-doctype-but-got-start-tag",
+        "(1,38): adoption-agency-1.3",
+        "(1,47): adoption-agency-1.3",
+        "(1,56): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "div": true,
+            "b": true,
+            "i": true,
+            "p": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "div",
+                    "children": [
+                      {
+                        "text": " abc "
+                      },
+                      {
+                        "tag": "b",
+                        "children": [
+                          {
+                            "text": " def "
+                          },
+                          {
+                            "tag": "i",
+                            "children": [
+                              {
+                                "text": " ghi "
+                              }
+                            ]
+                          }
+                        ]
+                      },
+                      {
+                        "tag": "i"
+                      },
+                      {
+                        "tag": "p",
+                        "children": [
+                          {
+                            "tag": "i",
+                            "children": [
+                              {
+                                "tag": "b",
+                                "children": [
+                                  {
+                                    "text": " jkl "
+                                  }
+                                ]
+                              },
+                              {
+                                "text": " mno "
+                              }
+                            ]
+                          },
+                          {
+                            "text": " pqr "
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><div> abc <b> def <i> ghi </i></b><i></i><p><i><b> jkl </b> mno </i> pqr </p></div></body></html>",
+        "noQuirksBodyHtml": "<div> abc <b> def <i> ghi </i></b><i></i><p><i><b> jkl </b> mno </i> pqr </p></div>"
+      }
+    },
+    {
+      "data": "<DIV> abc <B> def <I> ghi <P> jkl </B> mno </I> pqr </P> stu",
+      "errors": [
+        "(1,5): expected-doctype-but-got-start-tag",
+        "(1,38): adoption-agency-1.3",
+        "(1,47): adoption-agency-1.3",
+        "(1,60): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "div": true,
+            "b": true,
+            "i": true,
+            "p": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "div",
+                    "children": [
+                      {
+                        "text": " abc "
+                      },
+                      {
+                        "tag": "b",
+                        "children": [
+                          {
+                            "text": " def "
+                          },
+                          {
+                            "tag": "i",
+                            "children": [
+                              {
+                                "text": " ghi "
+                              }
+                            ]
+                          }
+                        ]
+                      },
+                      {
+                        "tag": "i"
+                      },
+                      {
+                        "tag": "p",
+                        "children": [
+                          {
+                            "tag": "i",
+                            "children": [
+                              {
+                                "tag": "b",
+                                "children": [
+                                  {
+                                    "text": " jkl "
+                                  }
+                                ]
+                              },
+                              {
+                                "text": " mno "
+                              }
+                            ]
+                          },
+                          {
+                            "text": " pqr "
+                          }
+                        ]
+                      },
+                      {
+                        "text": " stu"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><div> abc <b> def <i> ghi </i></b><i></i><p><i><b> jkl </b> mno </i> pqr </p> stu</div></body></html>",
+        "noQuirksBodyHtml": "<div> abc <b> def <i> ghi </i></b><i></i><p><i><b> jkl </b> mno </i> pqr </p> stu</div>"
+      }
+    },
+    {
+      "data": "<test attribute---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------->",
+      "errors": [
+        "(1,1040): expected-doctype-but-got-start-tag",
+        "(1,1040): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "test": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "test",
+                    "attrs": [
+                      {
+                        "name": "attribute----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------",
+                        "value": ""
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><test attribute----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------=\"\"></test></body></html>",
+        "noQuirksBodyHtml": "<test attribute----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------=\"\"></test>"
+      }
+    },
+    {
+      "data": "<a href=\"blah\">aba<table><a href=\"foo\">br<tr><td></td></tr>x</table>aoe",
+      "errors": [
+        "(1,15): expected-doctype-but-got-start-tag",
+        "(1,39): unexpected-start-tag-implies-table-voodoo",
+        "(1,39): unexpected-start-tag-implies-end-tag",
+        "(1,39): unexpected-end-tag",
+        "(1,45): foster-parenting-character-in-table",
+        "(1,45): foster-parenting-character-in-table",
+        "(1,68): foster-parenting-character-in-table",
+        "(1,71): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "a": true,
+            "table": true,
+            "tbody": true,
+            "tr": true,
+            "td": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "a",
+                    "attrs": [
+                      {
+                        "name": "href",
+                        "value": "blah"
+                      }
+                    ],
+                    "children": [
+                      {
+                        "text": "aba"
+                      },
+                      {
+                        "tag": "a",
+                        "attrs": [
+                          {
+                            "name": "href",
+                            "value": "foo"
+                          }
+                        ],
+                        "children": [
+                          {
+                            "text": "br"
+                          }
+                        ]
+                      },
+                      {
+                        "tag": "a",
+                        "attrs": [
+                          {
+                            "name": "href",
+                            "value": "foo"
+                          }
+                        ],
+                        "children": [
+                          {
+                            "text": "x"
+                          }
+                        ]
+                      },
+                      {
+                        "tag": "table",
+                        "children": [
+                          {
+                            "tag": "tbody",
+                            "children": [
+                              {
+                                "tag": "tr",
+                                "children": [
+                                  {
+                                    "tag": "td"
+                                  }
+                                ]
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "a",
+                    "attrs": [
+                      {
+                        "name": "href",
+                        "value": "foo"
+                      }
+                    ],
+                    "children": [
+                      {
+                        "text": "aoe"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><a href=\"blah\">aba<a href=\"foo\">br</a><a href=\"foo\">x</a><table><tbody><tr><td></td></tr></tbody></table></a><a href=\"foo\">aoe</a></body></html>",
+        "noQuirksBodyHtml": "<a href=\"blah\">aba<a href=\"foo\">br</a><a href=\"foo\">x</a><table><tbody><tr><td></td></tr></tbody></table></a><a href=\"foo\">aoe</a>"
+      }
+    },
+    {
+      "data": "<a href=\"blah\">aba<table><tr><td><a href=\"foo\">br</td></tr>x</table>aoe",
+      "errors": [
+        "(1,15): expected-doctype-but-got-start-tag",
+        "(1,54): unexpected-cell-end-tag",
+        "(1,68): unexpected text in table",
+        "(1,71): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "a": true,
+            "table": true,
+            "tbody": true,
+            "tr": true,
+            "td": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "a",
+                    "attrs": [
+                      {
+                        "name": "href",
+                        "value": "blah"
+                      }
+                    ],
+                    "children": [
+                      {
+                        "text": "abax"
+                      },
+                      {
+                        "tag": "table",
+                        "children": [
+                          {
+                            "tag": "tbody",
+                            "children": [
+                              {
+                                "tag": "tr",
+                                "children": [
+                                  {
+                                    "tag": "td",
+                                    "children": [
+                                      {
+                                        "tag": "a",
+                                        "attrs": [
+                                          {
+                                            "name": "href",
+                                            "value": "foo"
+                                          }
+                                        ],
+                                        "children": [
+                                          {
+                                            "text": "br"
+                                          }
+                                        ]
+                                      }
+                                    ]
+                                  }
+                                ]
+                              }
+                            ]
+                          }
+                        ]
+                      },
+                      {
+                        "text": "aoe"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><a href=\"blah\">abax<table><tbody><tr><td><a href=\"foo\">br</a></td></tr></tbody></table>aoe</a></body></html>",
+        "noQuirksBodyHtml": "<a href=\"blah\">abax<table><tbody><tr><td><a href=\"foo\">br</a></td></tr></tbody></table>aoe</a>"
+      }
+    },
+    {
+      "data": "<table><a href=\"blah\">aba<tr><td><a href=\"foo\">br</td></tr>x</table>aoe",
+      "errors": [
+        "(1,7): expected-doctype-but-got-start-tag",
+        "(1,22): unexpected-start-tag-implies-table-voodoo",
+        "(1,29): foster-parenting-character-in-table",
+        "(1,29): foster-parenting-character-in-table",
+        "(1,29): foster-parenting-character-in-table",
+        "(1,54): unexpected-cell-end-tag",
+        "(1,68): foster-parenting-character-in-table",
+        "(1,71): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "a": true,
+            "table": true,
+            "tbody": true,
+            "tr": true,
+            "td": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "a",
+                    "attrs": [
+                      {
+                        "name": "href",
+                        "value": "blah"
+                      }
+                    ],
+                    "children": [
+                      {
+                        "text": "aba"
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "a",
+                    "attrs": [
+                      {
+                        "name": "href",
+                        "value": "blah"
+                      }
+                    ],
+                    "children": [
+                      {
+                        "text": "x"
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "tbody",
+                        "children": [
+                          {
+                            "tag": "tr",
+                            "children": [
+                              {
+                                "tag": "td",
+                                "children": [
+                                  {
+                                    "tag": "a",
+                                    "attrs": [
+                                      {
+                                        "name": "href",
+                                        "value": "foo"
+                                      }
+                                    ],
+                                    "children": [
+                                      {
+                                        "text": "br"
+                                      }
+                                    ]
+                                  }
+                                ]
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "a",
+                    "attrs": [
+                      {
+                        "name": "href",
+                        "value": "blah"
+                      }
+                    ],
+                    "children": [
+                      {
+                        "text": "aoe"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><a href=\"blah\">aba</a><a href=\"blah\">x</a><table><tbody><tr><td><a href=\"foo\">br</a></td></tr></tbody></table><a href=\"blah\">aoe</a></body></html>",
+        "noQuirksBodyHtml": "<a href=\"blah\">aba</a><a href=\"blah\">x</a><table><tbody><tr><td><a href=\"foo\">br</a></td></tr></tbody></table><a href=\"blah\">aoe</a>"
+      }
+    },
+    {
+      "data": "<a href=a>aa<marquee>aa<a href=b>bb</marquee>aa",
+      "errors": [
+        "(1,10): expected-doctype-but-got-start-tag",
+        "(1,45): end-tag-too-early",
+        "(1,47): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "a": true,
+            "marquee": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "a",
+                    "attrs": [
+                      {
+                        "name": "href",
+                        "value": "a"
+                      }
+                    ],
+                    "children": [
+                      {
+                        "text": "aa"
+                      },
+                      {
+                        "tag": "marquee",
+                        "children": [
+                          {
+                            "text": "aa"
+                          },
+                          {
+                            "tag": "a",
+                            "attrs": [
+                              {
+                                "name": "href",
+                                "value": "b"
+                              }
+                            ],
+                            "children": [
+                              {
+                                "text": "bb"
+                              }
+                            ]
+                          }
+                        ]
+                      },
+                      {
+                        "text": "aa"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><a href=\"a\">aa<marquee>aa<a href=\"b\">bb</a></marquee>aa</a></body></html>",
+        "noQuirksBodyHtml": "<a href=\"a\">aa<marquee>aa<a href=\"b\">bb</a></marquee>aa</a>"
+      }
+    },
+    {
+      "data": "<wbr><strike><code></strike><code><strike></code>",
+      "errors": [
+        "(1,5): expected-doctype-but-got-start-tag",
+        "(1,28): adoption-agency-1.3",
+        "(1,49): adoption-agency-1.3",
+        "(1,49): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "wbr": true,
+            "strike": true,
+            "code": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "wbr"
+                  },
+                  {
+                    "tag": "strike",
+                    "children": [
+                      {
+                        "tag": "code"
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "code",
+                    "children": [
+                      {
+                        "tag": "code",
+                        "children": [
+                          {
+                            "tag": "strike"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><wbr><strike><code></code></strike><code><code><strike></strike></code></code></body></html>",
+        "noQuirksBodyHtml": "<wbr><strike><code></code></strike><code><code><strike></strike></code></code>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><spacer>foo",
+      "errors": [
+        "(1,26): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "spacer": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "spacer",
+                    "children": [
+                      {
+                        "text": "foo"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><spacer>foo</spacer></body></html>",
+        "noQuirksBodyHtml": "<spacer>foo</spacer>"
+      }
+    },
+    {
+      "data": "<title><meta></title><link><title><meta></title>",
+      "errors": [
+        "(1,7): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "title": true,
+            "link": true,
+            "body": true
+          },
+          "escaped": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "title",
+                    "children": [
+                      {
+                        "text": "<meta>",
+                        "escaped": true
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "link"
+                  },
+                  {
+                    "tag": "title",
+                    "children": [
+                      {
+                        "text": "<meta>",
+                        "escaped": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><title>&lt;meta&gt;</title><link><title>&lt;meta&gt;</title></head><body></body></html>",
+        "noQuirksBodyHtml": "<title>&lt;meta&gt;</title><link><title>&lt;meta&gt;</title>"
+      }
+    },
+    {
+      "data": "<style><!--</style><meta><script>--><link></script>",
+      "errors": [
+        "(1,7): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "style": true,
+            "meta": true,
+            "script": true,
+            "body": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "style",
+                    "children": [
+                      {
+                        "text": "<!--",
+                        "no_escape": true
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "meta"
+                  },
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "--><link>",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><style><!--</style><meta><script>--><link></script></head><body></body></html>",
+        "noQuirksBodyHtml": "<style><!--</style><meta><script>--><link></script>"
+      }
+    },
+    {
+      "data": "<head><meta></head><link>",
+      "errors": [
+        "(1,6): expected-doctype-but-got-start-tag",
+        "(1,25): unexpected-start-tag-out-of-my-head"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "meta": true,
+            "link": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "meta"
+                  },
+                  {
+                    "tag": "link"
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><meta><link></head><body></body></html>",
+        "noQuirksBodyHtml": "<meta><link>"
+      }
+    },
+    {
+      "data": "<table><tr><tr><td><td><span><th><span>X</table>",
+      "errors": [
+        "(1,7): expected-doctype-but-got-start-tag",
+        "(1,33): unexpected-cell-end-tag",
+        "(1,48): unexpected-cell-end-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true,
+            "tbody": true,
+            "tr": true,
+            "td": true,
+            "span": true,
+            "th": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "tbody",
+                        "children": [
+                          {
+                            "tag": "tr"
+                          },
+                          {
+                            "tag": "tr",
+                            "children": [
+                              {
+                                "tag": "td"
+                              },
+                              {
+                                "tag": "td",
+                                "children": [
+                                  {
+                                    "tag": "span"
+                                  }
+                                ]
+                              },
+                              {
+                                "tag": "th",
+                                "children": [
+                                  {
+                                    "tag": "span",
+                                    "children": [
+                                      {
+                                        "text": "X"
+                                      }
+                                    ]
+                                  }
+                                ]
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><table><tbody><tr></tr><tr><td></td><td><span></span></td><th><span>X</span></th></tr></tbody></table></body></html>",
+        "noQuirksBodyHtml": "<table><tbody><tr></tr><tr><td></td><td><span></span></td><th><span>X</span></th></tr></tbody></table>"
+      }
+    },
+    {
+      "data": "<body><body><base><link><meta><title><p></title><body><p></body>",
+      "errors": [
+        "(1,6): expected-doctype-but-got-start-tag",
+        "(1,12): unexpected-start-tag",
+        "(1,54): unexpected-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "base": true,
+            "link": true,
+            "meta": true,
+            "title": true,
+            "p": true
+          },
+          "escaped": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "base"
+                  },
+                  {
+                    "tag": "link"
+                  },
+                  {
+                    "tag": "meta"
+                  },
+                  {
+                    "tag": "title",
+                    "children": [
+                      {
+                        "text": "<p>",
+                        "escaped": true
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "p"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><base><link><meta><title>&lt;p&gt;</title><p></p></body></html>",
+        "noQuirksBodyHtml": "<base><link><meta><title>&lt;p&gt;</title><p></p>"
+      }
+    },
+    {
+      "data": "<textarea><p></textarea>",
+      "errors": [
+        "(1,10): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "textarea": true
+          },
+          "escaped": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "textarea",
+                    "children": [
+                      {
+                        "text": "<p>",
+                        "escaped": true
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><textarea>&lt;p&gt;</textarea></body></html>",
+        "noQuirksBodyHtml": "<textarea>&lt;p&gt;</textarea>"
+      }
+    },
+    {
+      "data": "<p><image></p>",
+      "errors": [
+        "(1,3): expected-doctype-but-got-start-tag",
+        "(1,10): unexpected-start-tag-treated-as"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "p": true,
+            "img": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "p",
+                    "children": [
+                      {
+                        "tag": "img"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><p><img></p></body></html>",
+        "noQuirksBodyHtml": "<p><img></p>"
+      }
+    },
+    {
+      "data": "<a><table><a></table><p><a><div><a>",
+      "errors": [
+        "(1,3): expected-doctype-but-got-start-tag",
+        "(1,13): unexpected-start-tag-implies-table-voodoo",
+        "(1,13): unexpected-start-tag-implies-end-tag",
+        "(1,13): adoption-agency-1.3",
+        "(1,27): unexpected-start-tag-implies-end-tag",
+        "(1,27): adoption-agency-1.2",
+        "(1,32): unexpected-end-tag",
+        "(1,35): unexpected-start-tag-implies-end-tag",
+        "(1,35): adoption-agency-1.2",
+        "(1,35): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "a": true,
+            "table": true,
+            "p": true,
+            "div": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "a",
+                    "children": [
+                      {
+                        "tag": "a"
+                      },
+                      {
+                        "tag": "table"
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "p",
+                    "children": [
+                      {
+                        "tag": "a"
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "div",
+                    "children": [
+                      {
+                        "tag": "a"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><a><a></a><table></table></a><p><a></a></p><div><a></a></div></body></html>",
+        "noQuirksBodyHtml": "<a><a></a><table></table></a><p><a></a></p><div><a></a></div>"
+      }
+    },
+    {
+      "data": "<head></p><meta><p>",
+      "errors": [
+        "(1,6): expected-doctype-but-got-start-tag",
+        "(1,10): unexpected-end-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "meta": true,
+            "body": true,
+            "p": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "meta"
+                  }
+                ]
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "p"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><meta></head><body><p></p></body></html>",
+        "noQuirksBodyHtml": "<p></p><meta><p></p>"
+      }
+    },
+    {
+      "data": "<head></html><meta><p>",
+      "errors": [
+        "(1,6): expected-doctype-but-got-start-tag",
+        "(1,19): expected-eof-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "meta": true,
+            "p": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "meta"
+                  },
+                  {
+                    "tag": "p"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><meta><p></p></body></html>",
+        "noQuirksBodyHtml": "<meta><p></p>"
+      }
+    },
+    {
+      "data": "<b><table><td><i></table>",
+      "errors": [
+        "(1,3): expected-doctype-but-got-start-tag",
+        "(1,14): unexpected-cell-in-table-body",
+        "(1,25): unexpected-cell-end-tag",
+        "(1,25): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "b": true,
+            "table": true,
+            "tbody": true,
+            "tr": true,
+            "td": true,
+            "i": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "b",
+                    "children": [
+                      {
+                        "tag": "table",
+                        "children": [
+                          {
+                            "tag": "tbody",
+                            "children": [
+                              {
+                                "tag": "tr",
+                                "children": [
+                                  {
+                                    "tag": "td",
+                                    "children": [
+                                      {
+                                        "tag": "i"
+                                      }
+                                    ]
+                                  }
+                                ]
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><b><table><tbody><tr><td><i></i></td></tr></tbody></table></b></body></html>",
+        "noQuirksBodyHtml": "<b><table><tbody><tr><td><i></i></td></tr></tbody></table></b>"
+      }
+    },
+    {
+      "data": "<b><table><td></b><i></table>",
+      "errors": [
+        "(1,3): expected-doctype-but-got-start-tag",
+        "(1,14): unexpected-cell-in-table-body",
+        "(1,18): unexpected-end-tag",
+        "(1,29): unexpected-cell-end-tag",
+        "(1,29): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "b": true,
+            "table": true,
+            "tbody": true,
+            "tr": true,
+            "td": true,
+            "i": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "b",
+                    "children": [
+                      {
+                        "tag": "table",
+                        "children": [
+                          {
+                            "tag": "tbody",
+                            "children": [
+                              {
+                                "tag": "tr",
+                                "children": [
+                                  {
+                                    "tag": "td",
+                                    "children": [
+                                      {
+                                        "tag": "i"
+                                      }
+                                    ]
+                                  }
+                                ]
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><b><table><tbody><tr><td><i></i></td></tr></tbody></table></b></body></html>",
+        "noQuirksBodyHtml": "<b><table><tbody><tr><td><i></i></td></tr></tbody></table></b>"
+      }
+    },
+    {
+      "data": "<h1><h2>",
+      "errors": [
+        "(1,4): expected-doctype-but-got-start-tag",
+        "(1,8): unexpected-start-tag",
+        "(1,8): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "h1": true,
+            "h2": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "h1"
+                  },
+                  {
+                    "tag": "h2"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><h1></h1><h2></h2></body></html>",
+        "noQuirksBodyHtml": "<h1></h1><h2></h2>"
+      }
+    },
+    {
+      "data": "<a><p><a></a></p></a>",
+      "errors": [
+        "(1,3): expected-doctype-but-got-start-tag",
+        "(1,9): unexpected-start-tag-implies-end-tag",
+        "(1,9): adoption-agency-1.3",
+        "(1,21): unexpected-end-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "a": true,
+            "p": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "a"
+                  },
+                  {
+                    "tag": "p",
+                    "children": [
+                      {
+                        "tag": "a"
+                      },
+                      {
+                        "tag": "a"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><a></a><p><a></a><a></a></p></body></html>",
+        "noQuirksBodyHtml": "<a></a><p><a></a><a></a></p>"
+      }
+    },
+    {
+      "data": "<b><button></b></button></b>",
+      "errors": [
+        "(1,3): expected-doctype-but-got-start-tag",
+        "(1,15): adoption-agency-1.3",
+        "(1,28): unexpected-end-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "b": true,
+            "button": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "b"
+                  },
+                  {
+                    "tag": "button",
+                    "children": [
+                      {
+                        "tag": "b"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><b></b><button><b></b></button></body></html>",
+        "noQuirksBodyHtml": "<b></b><button><b></b></button>"
+      }
+    },
+    {
+      "data": "<p><b><div><marquee></p></b></div>",
+      "errors": [
+        "(1,3): expected-doctype-but-got-start-tag",
+        "(1,11): unexpected-end-tag",
+        "(1,24): unexpected-end-tag",
+        "(1,28): unexpected-end-tag",
+        "(1,34): end-tag-too-early",
+        "(1,34): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "p": true,
+            "b": true,
+            "div": true,
+            "marquee": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "p",
+                    "children": [
+                      {
+                        "tag": "b"
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "div",
+                    "children": [
+                      {
+                        "tag": "b",
+                        "children": [
+                          {
+                            "tag": "marquee",
+                            "children": [
+                              {
+                                "tag": "p"
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><p><b></b></p><div><b><marquee><p></p></marquee></b></div></body></html>",
+        "noQuirksBodyHtml": "<p><b></b></p><div><b><marquee><p></p></marquee></b></div>"
+      }
+    },
+    {
+      "data": "<script></script></div><title></title><p><p>",
+      "errors": [
+        "(1,8): expected-doctype-but-got-start-tag",
+        "(1,23): unexpected-end-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "title": true,
+            "body": true,
+            "p": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script"
+                  },
+                  {
+                    "tag": "title"
+                  }
+                ]
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "p"
+                  },
+                  {
+                    "tag": "p"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><script></script><title></title></head><body><p></p><p></p></body></html>",
+        "noQuirksBodyHtml": "<script></script><title></title><p></p><p></p>"
+      }
+    },
+    {
+      "data": "<p><hr></p>",
+      "errors": [
+        "(1,3): expected-doctype-but-got-start-tag",
+        "(1,11): unexpected-end-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "p": true,
+            "hr": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "p"
+                  },
+                  {
+                    "tag": "hr"
+                  },
+                  {
+                    "tag": "p"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><p></p><hr><p></p></body></html>",
+        "noQuirksBodyHtml": "<p></p><hr><p></p>"
+      }
+    },
+    {
+      "data": "<select><b><option><select><option></b></select>",
+      "errors": [
+        "(1,8): expected-doctype-but-got-start-tag",
+        "(1,11): unexpected-start-tag-in-select",
+        "(1,27): unexpected-select-in-select",
+        "(1,39): unexpected-end-tag",
+        "(1,48): unexpected-end-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "select": true,
+            "option": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "select",
+                    "children": [
+                      {
+                        "tag": "option"
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "option"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><select><option></option></select><option></option></body></html>",
+        "noQuirksBodyHtml": "<select><option></option></select><option></option>"
+      }
+    },
+    {
+      "data": "<html><head><title></title><body></body></html>",
+      "errors": [
+        "(1,6): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "title": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "title"
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><title></title></head><body></body></html>",
+        "noQuirksBodyHtml": "<title></title>"
+      }
+    },
+    {
+      "data": "<a><table><td><a><table></table><a></tr><a></table><a>",
+      "errors": [
+        "(1,3): expected-doctype-but-got-start-tag",
+        "(1,14): unexpected-cell-in-table-body",
+        "(1,35): unexpected-start-tag-implies-end-tag",
+        "(1,40): unexpected-cell-end-tag",
+        "(1,43): unexpected-start-tag-implies-table-voodoo",
+        "(1,43): unexpected-start-tag-implies-end-tag",
+        "(1,43): unexpected-end-tag",
+        "(1,54): unexpected-start-tag-implies-end-tag",
+        "(1,54): adoption-agency-1.2",
+        "(1,54): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "a": true,
+            "table": true,
+            "tbody": true,
+            "tr": true,
+            "td": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "a",
+                    "children": [
+                      {
+                        "tag": "a"
+                      },
+                      {
+                        "tag": "table",
+                        "children": [
+                          {
+                            "tag": "tbody",
+                            "children": [
+                              {
+                                "tag": "tr",
+                                "children": [
+                                  {
+                                    "tag": "td",
+                                    "children": [
+                                      {
+                                        "tag": "a",
+                                        "children": [
+                                          {
+                                            "tag": "table"
+                                          }
+                                        ]
+                                      },
+                                      {
+                                        "tag": "a"
+                                      }
+                                    ]
+                                  }
+                                ]
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "a"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><a><a></a><table><tbody><tr><td><a><table></table></a><a></a></td></tr></tbody></table></a><a></a></body></html>",
+        "noQuirksBodyHtml": "<a><a></a><table><tbody><tr><td><a><table></table></a><a></a></td></tr></tbody></table></a><a></a>"
+      }
+    },
+    {
+      "data": "<ul><li></li><div><li></div><li><li><div><li><address><li><b><em></b><li></ul>",
+      "errors": [
+        "(1,4): expected-doctype-but-got-start-tag",
+        "(1,45): end-tag-too-early",
+        "(1,58): end-tag-too-early",
+        "(1,69): adoption-agency-1.3"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "ul": true,
+            "li": true,
+            "div": true,
+            "address": true,
+            "b": true,
+            "em": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "ul",
+                    "children": [
+                      {
+                        "tag": "li"
+                      },
+                      {
+                        "tag": "div",
+                        "children": [
+                          {
+                            "tag": "li"
+                          }
+                        ]
+                      },
+                      {
+                        "tag": "li"
+                      },
+                      {
+                        "tag": "li",
+                        "children": [
+                          {
+                            "tag": "div"
+                          }
+                        ]
+                      },
+                      {
+                        "tag": "li",
+                        "children": [
+                          {
+                            "tag": "address"
+                          }
+                        ]
+                      },
+                      {
+                        "tag": "li",
+                        "children": [
+                          {
+                            "tag": "b",
+                            "children": [
+                              {
+                                "tag": "em"
+                              }
+                            ]
+                          }
+                        ]
+                      },
+                      {
+                        "tag": "li"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><ul><li></li><div><li></li></div><li></li><li><div></div></li><li><address></address></li><li><b><em></em></b></li><li></li></ul></body></html>",
+        "noQuirksBodyHtml": "<ul><li></li><div><li></li></div><li></li><li><div></div></li><li><address></address></li><li><b><em></em></b></li><li></li></ul>"
+      }
+    },
+    {
+      "data": "<ul><li><ul></li><li>a</li></ul></li></ul>",
+      "errors": [
+        "(1,4): expected-doctype-but-got-start-tag",
+        "(1,17): unexpected-end-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "ul": true,
+            "li": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "ul",
+                    "children": [
+                      {
+                        "tag": "li",
+                        "children": [
+                          {
+                            "tag": "ul",
+                            "children": [
+                              {
+                                "tag": "li",
+                                "children": [
+                                  {
+                                    "text": "a"
+                                  }
+                                ]
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><ul><li><ul><li>a</li></ul></li></ul></body></html>",
+        "noQuirksBodyHtml": "<ul><li><ul><li>a</li></ul></li></ul>"
+      }
+    },
+    {
+      "data": "<frameset><frame><frameset><frame></frameset><noframes></noframes></frameset>",
+      "errors": [
+        "(1,10): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "frameset": true,
+            "frame": true,
+            "noframes": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "frameset",
+                "children": [
+                  {
+                    "tag": "frame"
+                  },
+                  {
+                    "tag": "frameset",
+                    "children": [
+                      {
+                        "tag": "frame"
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "noframes"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><frameset><frame><frameset><frame></frameset><noframes></noframes></frameset></html>",
+        "noQuirksBodyHtml": "<noframes></noframes>"
+      }
+    },
+    {
+      "data": "<h1><table><td><h3></table><h3></h1>",
+      "errors": [
+        "(1,4): expected-doctype-but-got-start-tag",
+        "(1,15): unexpected-cell-in-table-body",
+        "(1,27): unexpected-cell-end-tag",
+        "(1,31): unexpected-start-tag",
+        "(1,36): end-tag-too-early"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "h1": true,
+            "table": true,
+            "tbody": true,
+            "tr": true,
+            "td": true,
+            "h3": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "h1",
+                    "children": [
+                      {
+                        "tag": "table",
+                        "children": [
+                          {
+                            "tag": "tbody",
+                            "children": [
+                              {
+                                "tag": "tr",
+                                "children": [
+                                  {
+                                    "tag": "td",
+                                    "children": [
+                                      {
+                                        "tag": "h3"
+                                      }
+                                    ]
+                                  }
+                                ]
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "h3"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><h1><table><tbody><tr><td><h3></h3></td></tr></tbody></table></h1><h3></h3></body></html>",
+        "noQuirksBodyHtml": "<h1><table><tbody><tr><td><h3></h3></td></tr></tbody></table></h1><h3></h3>"
+      }
+    },
+    {
+      "data": "<table><colgroup><col><colgroup><col><col><col><colgroup><col><col><thead><tr><td></table>",
+      "errors": [
+        "(1,7): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true,
+            "colgroup": true,
+            "col": true,
+            "thead": true,
+            "tr": true,
+            "td": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "colgroup",
+                        "children": [
+                          {
+                            "tag": "col"
+                          }
+                        ]
+                      },
+                      {
+                        "tag": "colgroup",
+                        "children": [
+                          {
+                            "tag": "col"
+                          },
+                          {
+                            "tag": "col"
+                          },
+                          {
+                            "tag": "col"
+                          }
+                        ]
+                      },
+                      {
+                        "tag": "colgroup",
+                        "children": [
+                          {
+                            "tag": "col"
+                          },
+                          {
+                            "tag": "col"
+                          }
+                        ]
+                      },
+                      {
+                        "tag": "thead",
+                        "children": [
+                          {
+                            "tag": "tr",
+                            "children": [
+                              {
+                                "tag": "td"
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><table><colgroup><col></colgroup><colgroup><col><col><col></colgroup><colgroup><col><col></colgroup><thead><tr><td></td></tr></thead></table></body></html>",
+        "noQuirksBodyHtml": "<table><colgroup><col></colgroup><colgroup><col><col><col></colgroup><colgroup><col><col></colgroup><thead><tr><td></td></tr></thead></table>"
+      }
+    },
+    {
+      "data": "<table><col><tbody><col><tr><col><td><col></table><col>",
+      "errors": [
+        "(1,7): expected-doctype-but-got-start-tag",
+        "(1,37): unexpected-cell-in-table-body",
+        "(1,55): unexpected-start-tag-ignored"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true,
+            "colgroup": true,
+            "col": true,
+            "tbody": true,
+            "tr": true,
+            "td": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "colgroup",
+                        "children": [
+                          {
+                            "tag": "col"
+                          }
+                        ]
+                      },
+                      {
+                        "tag": "tbody"
+                      },
+                      {
+                        "tag": "colgroup",
+                        "children": [
+                          {
+                            "tag": "col"
+                          }
+                        ]
+                      },
+                      {
+                        "tag": "tbody",
+                        "children": [
+                          {
+                            "tag": "tr"
+                          }
+                        ]
+                      },
+                      {
+                        "tag": "colgroup",
+                        "children": [
+                          {
+                            "tag": "col"
+                          }
+                        ]
+                      },
+                      {
+                        "tag": "tbody",
+                        "children": [
+                          {
+                            "tag": "tr",
+                            "children": [
+                              {
+                                "tag": "td"
+                              }
+                            ]
+                          }
+                        ]
+                      },
+                      {
+                        "tag": "colgroup",
+                        "children": [
+                          {
+                            "tag": "col"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><table><colgroup><col></colgroup><tbody></tbody><colgroup><col></colgroup><tbody><tr></tr></tbody><colgroup><col></colgroup><tbody><tr><td></td></tr></tbody><colgroup><col></colgroup></table></body></html>",
+        "noQuirksBodyHtml": "<table><colgroup><col></colgroup><tbody></tbody><colgroup><col></colgroup><tbody><tr></tr></tbody><colgroup><col></colgroup><tbody><tr><td></td></tr></tbody><colgroup><col></colgroup></table>"
+      }
+    },
+    {
+      "data": "<table><colgroup><tbody><colgroup><tr><colgroup><td><colgroup></table><colgroup>",
+      "errors": [
+        "(1,7): expected-doctype-but-got-start-tag",
+        "(1,52): unexpected-cell-in-table-body",
+        "(1,80): unexpected-start-tag-ignored"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true,
+            "colgroup": true,
+            "tbody": true,
+            "tr": true,
+            "td": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "colgroup"
+                      },
+                      {
+                        "tag": "tbody"
+                      },
+                      {
+                        "tag": "colgroup"
+                      },
+                      {
+                        "tag": "tbody",
+                        "children": [
+                          {
+                            "tag": "tr"
+                          }
+                        ]
+                      },
+                      {
+                        "tag": "colgroup"
+                      },
+                      {
+                        "tag": "tbody",
+                        "children": [
+                          {
+                            "tag": "tr",
+                            "children": [
+                              {
+                                "tag": "td"
+                              }
+                            ]
+                          }
+                        ]
+                      },
+                      {
+                        "tag": "colgroup"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><table><colgroup></colgroup><tbody></tbody><colgroup></colgroup><tbody><tr></tr></tbody><colgroup></colgroup><tbody><tr><td></td></tr></tbody><colgroup></colgroup></table></body></html>",
+        "noQuirksBodyHtml": "<table><colgroup></colgroup><tbody></tbody><colgroup></colgroup><tbody><tr></tr></tbody><colgroup></colgroup><tbody><tr><td></td></tr></tbody><colgroup></colgroup></table>"
+      }
+    },
+    {
+      "data": "</strong></b></em></i></u></strike></s></blink></tt></pre></big></small></font></select></h1></h2></h3></h4></h5></h6></body></br></a></img></title></span></style></script></table></th></td></tr></frame></area></link></param></hr></input></col></base></meta></basefont></bgsound></embed></spacer></p></dd></dt></caption></colgroup></tbody></tfoot></thead></address></blockquote></center></dir></div></dl></fieldset></listing></menu></ol></ul></li></nobr></wbr></form></button></marquee></object></html></frameset></head></iframe></image></isindex></noembed></noframes></noscript></optgroup></option></plaintext></textarea>",
+      "errors": [
+        "(1,9): expected-doctype-but-got-end-tag",
+        "(1,9): unexpected-end-tag-before-html",
+        "(1,13): unexpected-end-tag-before-html",
+        "(1,18): unexpected-end-tag-before-html",
+        "(1,22): unexpected-end-tag-before-html",
+        "(1,26): unexpected-end-tag-before-html",
+        "(1,35): unexpected-end-tag-before-html",
+        "(1,39): unexpected-end-tag-before-html",
+        "(1,47): unexpected-end-tag-before-html",
+        "(1,52): unexpected-end-tag-before-html",
+        "(1,58): unexpected-end-tag-before-html",
+        "(1,64): unexpected-end-tag-before-html",
+        "(1,72): unexpected-end-tag-before-html",
+        "(1,79): unexpected-end-tag-before-html",
+        "(1,88): unexpected-end-tag-before-html",
+        "(1,93): unexpected-end-tag-before-html",
+        "(1,98): unexpected-end-tag-before-html",
+        "(1,103): unexpected-end-tag-before-html",
+        "(1,108): unexpected-end-tag-before-html",
+        "(1,113): unexpected-end-tag-before-html",
+        "(1,118): unexpected-end-tag-before-html",
+        "(1,130): unexpected-end-tag-after-body",
+        "(1,130): unexpected-end-tag-treated-as",
+        "(1,134): unexpected-end-tag",
+        "(1,140): unexpected-end-tag",
+        "(1,148): unexpected-end-tag",
+        "(1,155): unexpected-end-tag",
+        "(1,163): unexpected-end-tag",
+        "(1,172): unexpected-end-tag",
+        "(1,180): unexpected-end-tag",
+        "(1,185): unexpected-end-tag",
+        "(1,190): unexpected-end-tag",
+        "(1,195): unexpected-end-tag",
+        "(1,203): unexpected-end-tag",
+        "(1,210): unexpected-end-tag",
+        "(1,217): unexpected-end-tag",
+        "(1,225): unexpected-end-tag",
+        "(1,230): unexpected-end-tag",
+        "(1,238): unexpected-end-tag",
+        "(1,244): unexpected-end-tag",
+        "(1,251): unexpected-end-tag",
+        "(1,258): unexpected-end-tag",
+        "(1,269): unexpected-end-tag",
+        "(1,279): unexpected-end-tag",
+        "(1,287): unexpected-end-tag",
+        "(1,296): unexpected-end-tag",
+        "(1,300): unexpected-end-tag",
+        "(1,305): unexpected-end-tag",
+        "(1,310): unexpected-end-tag",
+        "(1,320): unexpected-end-tag",
+        "(1,331): unexpected-end-tag",
+        "(1,339): unexpected-end-tag",
+        "(1,347): unexpected-end-tag",
+        "(1,355): unexpected-end-tag",
+        "(1,365): end-tag-too-early",
+        "(1,378): end-tag-too-early",
+        "(1,387): end-tag-too-early",
+        "(1,393): end-tag-too-early",
+        "(1,399): end-tag-too-early",
+        "(1,404): end-tag-too-early",
+        "(1,415): end-tag-too-early",
+        "(1,425): end-tag-too-early",
+        "(1,432): end-tag-too-early",
+        "(1,437): end-tag-too-early",
+        "(1,442): end-tag-too-early",
+        "(1,447): unexpected-end-tag",
+        "(1,454): unexpected-end-tag",
+        "(1,460): unexpected-end-tag",
+        "(1,467): unexpected-end-tag",
+        "(1,476): end-tag-too-early",
+        "(1,486): end-tag-too-early",
+        "(1,495): end-tag-too-early",
+        "(1,513): expected-eof-but-got-end-tag",
+        "(1,513): unexpected-end-tag",
+        "(1,520): unexpected-end-tag",
+        "(1,529): unexpected-end-tag",
+        "(1,537): unexpected-end-tag",
+        "(1,547): unexpected-end-tag",
+        "(1,557): unexpected-end-tag",
+        "(1,568): unexpected-end-tag",
+        "(1,579): unexpected-end-tag",
+        "(1,590): unexpected-end-tag",
+        "(1,599): unexpected-end-tag",
+        "(1,611): unexpected-end-tag",
+        "(1,622): unexpected-end-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "br": true,
+            "p": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "br"
+                  },
+                  {
+                    "tag": "p"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><br><p></p></body></html>",
+        "noQuirksBodyHtml": "<br><p></p>"
+      }
+    },
+    {
+      "data": "<table><tr></strong></b></em></i></u></strike></s></blink></tt></pre></big></small></font></select></h1></h2></h3></h4></h5></h6></body></br></a></img></title></span></style></script></table></th></td></tr></frame></area></link></param></hr></input></col></base></meta></basefont></bgsound></embed></spacer></p></dd></dt></caption></colgroup></tbody></tfoot></thead></address></blockquote></center></dir></div></dl></fieldset></listing></menu></ol></ul></li></nobr></wbr></form></button></marquee></object></html></frameset></head></iframe></image></isindex></noembed></noframes></noscript></optgroup></option></plaintext></textarea>",
+      "errors": [
+        "(1,7): expected-doctype-but-got-start-tag",
+        "(1,20): unexpected-end-tag-implies-table-voodoo",
+        "(1,20): unexpected-end-tag",
+        "(1,24): unexpected-end-tag-implies-table-voodoo",
+        "(1,24): unexpected-end-tag",
+        "(1,29): unexpected-end-tag-implies-table-voodoo",
+        "(1,29): unexpected-end-tag",
+        "(1,33): unexpected-end-tag-implies-table-voodoo",
+        "(1,33): unexpected-end-tag",
+        "(1,37): unexpected-end-tag-implies-table-voodoo",
+        "(1,37): unexpected-end-tag",
+        "(1,46): unexpected-end-tag-implies-table-voodoo",
+        "(1,46): unexpected-end-tag",
+        "(1,50): unexpected-end-tag-implies-table-voodoo",
+        "(1,50): unexpected-end-tag",
+        "(1,58): unexpected-end-tag-implies-table-voodoo",
+        "(1,58): unexpected-end-tag",
+        "(1,63): unexpected-end-tag-implies-table-voodoo",
+        "(1,63): unexpected-end-tag",
+        "(1,69): unexpected-end-tag-implies-table-voodoo",
+        "(1,69): end-tag-too-early",
+        "(1,75): unexpected-end-tag-implies-table-voodoo",
+        "(1,75): unexpected-end-tag",
+        "(1,83): unexpected-end-tag-implies-table-voodoo",
+        "(1,83): unexpected-end-tag",
+        "(1,90): unexpected-end-tag-implies-table-voodoo",
+        "(1,90): unexpected-end-tag",
+        "(1,99): unexpected-end-tag-implies-table-voodoo",
+        "(1,99): unexpected-end-tag",
+        "(1,104): unexpected-end-tag-implies-table-voodoo",
+        "(1,104): end-tag-too-early",
+        "(1,109): unexpected-end-tag-implies-table-voodoo",
+        "(1,109): end-tag-too-early",
+        "(1,114): unexpected-end-tag-implies-table-voodoo",
+        "(1,114): end-tag-too-early",
+        "(1,119): unexpected-end-tag-implies-table-voodoo",
+        "(1,119): end-tag-too-early",
+        "(1,124): unexpected-end-tag-implies-table-voodoo",
+        "(1,124): end-tag-too-early",
+        "(1,129): unexpected-end-tag-implies-table-voodoo",
+        "(1,129): end-tag-too-early",
+        "(1,136): unexpected-end-tag-in-table-row",
+        "(1,141): unexpected-end-tag-implies-table-voodoo",
+        "(1,141): unexpected-end-tag-treated-as",
+        "(1,145): unexpected-end-tag-implies-table-voodoo",
+        "(1,145): unexpected-end-tag",
+        "(1,151): unexpected-end-tag-implies-table-voodoo",
+        "(1,151): unexpected-end-tag",
+        "(1,159): unexpected-end-tag-implies-table-voodoo",
+        "(1,159): unexpected-end-tag",
+        "(1,166): unexpected-end-tag-implies-table-voodoo",
+        "(1,166): unexpected-end-tag",
+        "(1,174): unexpected-end-tag-implies-table-voodoo",
+        "(1,174): unexpected-end-tag",
+        "(1,183): unexpected-end-tag-implies-table-voodoo",
+        "(1,183): unexpected-end-tag",
+        "(1,196): unexpected-end-tag",
+        "(1,201): unexpected-end-tag",
+        "(1,206): unexpected-end-tag",
+        "(1,214): unexpected-end-tag",
+        "(1,221): unexpected-end-tag",
+        "(1,228): unexpected-end-tag",
+        "(1,236): unexpected-end-tag",
+        "(1,241): unexpected-end-tag",
+        "(1,249): unexpected-end-tag",
+        "(1,255): unexpected-end-tag",
+        "(1,262): unexpected-end-tag",
+        "(1,269): unexpected-end-tag",
+        "(1,280): unexpected-end-tag",
+        "(1,290): unexpected-end-tag",
+        "(1,298): unexpected-end-tag",
+        "(1,307): unexpected-end-tag",
+        "(1,311): unexpected-end-tag",
+        "(1,316): unexpected-end-tag",
+        "(1,321): unexpected-end-tag",
+        "(1,331): unexpected-end-tag",
+        "(1,342): unexpected-end-tag",
+        "(1,350): unexpected-end-tag",
+        "(1,358): unexpected-end-tag",
+        "(1,366): unexpected-end-tag",
+        "(1,376): end-tag-too-early",
+        "(1,389): end-tag-too-early",
+        "(1,398): end-tag-too-early",
+        "(1,404): end-tag-too-early",
+        "(1,410): end-tag-too-early",
+        "(1,415): end-tag-too-early",
+        "(1,426): end-tag-too-early",
+        "(1,436): end-tag-too-early",
+        "(1,443): end-tag-too-early",
+        "(1,448): end-tag-too-early",
+        "(1,453): end-tag-too-early",
+        "(1,458): unexpected-end-tag",
+        "(1,465): unexpected-end-tag",
+        "(1,471): unexpected-end-tag",
+        "(1,478): unexpected-end-tag",
+        "(1,487): end-tag-too-early",
+        "(1,497): end-tag-too-early",
+        "(1,506): end-tag-too-early",
+        "(1,524): expected-eof-but-got-end-tag",
+        "(1,524): unexpected-end-tag",
+        "(1,531): unexpected-end-tag",
+        "(1,540): unexpected-end-tag",
+        "(1,548): unexpected-end-tag",
+        "(1,558): unexpected-end-tag",
+        "(1,568): unexpected-end-tag",
+        "(1,579): unexpected-end-tag",
+        "(1,590): unexpected-end-tag",
+        "(1,601): unexpected-end-tag",
+        "(1,610): unexpected-end-tag",
+        "(1,622): unexpected-end-tag",
+        "(1,633): unexpected-end-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "br": true,
+            "table": true,
+            "tbody": true,
+            "tr": true,
+            "p": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "br"
+                  },
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "tbody",
+                        "children": [
+                          {
+                            "tag": "tr"
+                          }
+                        ]
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "p"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><br><table><tbody><tr></tr></tbody></table><p></p></body></html>",
+        "noQuirksBodyHtml": "<br><table><tbody><tr></tr></tbody></table><p></p>"
+      }
+    },
+    {
+      "data": "<frameset>",
+      "errors": [
+        "(1,10): expected-doctype-but-got-start-tag",
+        "(1,10): eof-in-frameset"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "frameset": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "frameset"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><frameset></frameset></html>",
+        "noQuirksBodyHtml": ""
+      }
+    }
+  ],
+  "tests10.dat": [
+    {
+      "data": "<!DOCTYPE html><svg></svg>",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "svg svg": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "svg",
+                    "ns": "http://www.w3.org/2000/svg"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><svg></svg></body></html>",
+        "noQuirksBodyHtml": "<svg></svg>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><svg></svg><![CDATA[a]]>",
+      "errors": [
+        "(1,28) expected-dashes-or-doctype"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "svg svg": true
+          },
+          "doctype": true,
+          "comment": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "svg",
+                    "ns": "http://www.w3.org/2000/svg"
+                  },
+                  {
+                    "comment": "[CDATA[a]]"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><svg></svg><!--[CDATA[a]]--></body></html>",
+        "noQuirksBodyHtml": "<svg></svg><!--[CDATA[a]]-->"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><body><svg></svg>",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "svg svg": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "svg",
+                    "ns": "http://www.w3.org/2000/svg"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><svg></svg></body></html>",
+        "noQuirksBodyHtml": "<svg></svg>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><body><select><svg></svg></select>",
+      "errors": [
+        "(1,34) unexpected-start-tag-in-select",
+        "(1,40) unexpected-end-tag-in-select"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "select": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "select"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><select></select></body></html>",
+        "noQuirksBodyHtml": "<select></select>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><body><select><option><svg></svg></option></select>",
+      "errors": [
+        "(1,42) unexpected-start-tag-in-select",
+        "(1,48) unexpected-end-tag-in-select"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "select": true,
+            "option": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "select",
+                    "children": [
+                      {
+                        "tag": "option"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><select><option></option></select></body></html>",
+        "noQuirksBodyHtml": "<select><option></option></select>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><body><table><svg></svg></table>",
+      "errors": [
+        "(1,33) foster-parenting-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "svg svg": true,
+            "table": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "svg",
+                    "ns": "http://www.w3.org/2000/svg"
+                  },
+                  {
+                    "tag": "table"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><svg></svg><table></table></body></html>",
+        "noQuirksBodyHtml": "<svg></svg><table></table>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><body><table><svg><g>foo</g></svg></table>",
+      "errors": [
+        "(1,33) foster-parenting-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "svg svg": true,
+            "svg g": true,
+            "table": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "svg",
+                    "ns": "http://www.w3.org/2000/svg",
+                    "children": [
+                      {
+                        "tag": "g",
+                        "ns": "http://www.w3.org/2000/svg",
+                        "children": [
+                          {
+                            "text": "foo"
+                          }
+                        ]
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "table"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><svg><g>foo</g></svg><table></table></body></html>",
+        "noQuirksBodyHtml": "<svg><g>foo</g></svg><table></table>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><body><table><svg><g>foo</g><g>bar</g></svg></table>",
+      "errors": [
+        "(1,33) foster-parenting-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "svg svg": true,
+            "svg g": true,
+            "table": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "svg",
+                    "ns": "http://www.w3.org/2000/svg",
+                    "children": [
+                      {
+                        "tag": "g",
+                        "ns": "http://www.w3.org/2000/svg",
+                        "children": [
+                          {
+                            "text": "foo"
+                          }
+                        ]
+                      },
+                      {
+                        "tag": "g",
+                        "ns": "http://www.w3.org/2000/svg",
+                        "children": [
+                          {
+                            "text": "bar"
+                          }
+                        ]
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "table"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><svg><g>foo</g><g>bar</g></svg><table></table></body></html>",
+        "noQuirksBodyHtml": "<svg><g>foo</g><g>bar</g></svg><table></table>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><body><table><tbody><svg><g>foo</g><g>bar</g></svg></tbody></table>",
+      "errors": [
+        "(1,40) foster-parenting-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "svg svg": true,
+            "svg g": true,
+            "table": true,
+            "tbody": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "svg",
+                    "ns": "http://www.w3.org/2000/svg",
+                    "children": [
+                      {
+                        "tag": "g",
+                        "ns": "http://www.w3.org/2000/svg",
+                        "children": [
+                          {
+                            "text": "foo"
+                          }
+                        ]
+                      },
+                      {
+                        "tag": "g",
+                        "ns": "http://www.w3.org/2000/svg",
+                        "children": [
+                          {
+                            "text": "bar"
+                          }
+                        ]
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "tbody"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><svg><g>foo</g><g>bar</g></svg><table><tbody></tbody></table></body></html>",
+        "noQuirksBodyHtml": "<svg><g>foo</g><g>bar</g></svg><table><tbody></tbody></table>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><body><table><tbody><tr><svg><g>foo</g><g>bar</g></svg></tr></tbody></table>",
+      "errors": [
+        "(1,44) foster-parenting-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "svg svg": true,
+            "svg g": true,
+            "table": true,
+            "tbody": true,
+            "tr": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "svg",
+                    "ns": "http://www.w3.org/2000/svg",
+                    "children": [
+                      {
+                        "tag": "g",
+                        "ns": "http://www.w3.org/2000/svg",
+                        "children": [
+                          {
+                            "text": "foo"
+                          }
+                        ]
+                      },
+                      {
+                        "tag": "g",
+                        "ns": "http://www.w3.org/2000/svg",
+                        "children": [
+                          {
+                            "text": "bar"
+                          }
+                        ]
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "tbody",
+                        "children": [
+                          {
+                            "tag": "tr"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><svg><g>foo</g><g>bar</g></svg><table><tbody><tr></tr></tbody></table></body></html>",
+        "noQuirksBodyHtml": "<svg><g>foo</g><g>bar</g></svg><table><tbody><tr></tr></tbody></table>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><body><table><tbody><tr><td><svg><g>foo</g><g>bar</g></svg></td></tr></tbody></table>",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true,
+            "tbody": true,
+            "tr": true,
+            "td": true,
+            "svg svg": true,
+            "svg g": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "tbody",
+                        "children": [
+                          {
+                            "tag": "tr",
+                            "children": [
+                              {
+                                "tag": "td",
+                                "children": [
+                                  {
+                                    "tag": "svg",
+                                    "ns": "http://www.w3.org/2000/svg",
+                                    "children": [
+                                      {
+                                        "tag": "g",
+                                        "ns": "http://www.w3.org/2000/svg",
+                                        "children": [
+                                          {
+                                            "text": "foo"
+                                          }
+                                        ]
+                                      },
+                                      {
+                                        "tag": "g",
+                                        "ns": "http://www.w3.org/2000/svg",
+                                        "children": [
+                                          {
+                                            "text": "bar"
+                                          }
+                                        ]
+                                      }
+                                    ]
+                                  }
+                                ]
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><table><tbody><tr><td><svg><g>foo</g><g>bar</g></svg></td></tr></tbody></table></body></html>",
+        "noQuirksBodyHtml": "<table><tbody><tr><td><svg><g>foo</g><g>bar</g></svg></td></tr></tbody></table>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><body><table><tbody><tr><td><svg><g>foo</g><g>bar</g></svg><p>baz</td></tr></tbody></table>",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true,
+            "tbody": true,
+            "tr": true,
+            "td": true,
+            "svg svg": true,
+            "svg g": true,
+            "p": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "tbody",
+                        "children": [
+                          {
+                            "tag": "tr",
+                            "children": [
+                              {
+                                "tag": "td",
+                                "children": [
+                                  {
+                                    "tag": "svg",
+                                    "ns": "http://www.w3.org/2000/svg",
+                                    "children": [
+                                      {
+                                        "tag": "g",
+                                        "ns": "http://www.w3.org/2000/svg",
+                                        "children": [
+                                          {
+                                            "text": "foo"
+                                          }
+                                        ]
+                                      },
+                                      {
+                                        "tag": "g",
+                                        "ns": "http://www.w3.org/2000/svg",
+                                        "children": [
+                                          {
+                                            "text": "bar"
+                                          }
+                                        ]
+                                      }
+                                    ]
+                                  },
+                                  {
+                                    "tag": "p",
+                                    "children": [
+                                      {
+                                        "text": "baz"
+                                      }
+                                    ]
+                                  }
+                                ]
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><table><tbody><tr><td><svg><g>foo</g><g>bar</g></svg><p>baz</p></td></tr></tbody></table></body></html>",
+        "noQuirksBodyHtml": "<table><tbody><tr><td><svg><g>foo</g><g>bar</g></svg><p>baz</p></td></tr></tbody></table>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><body><table><caption><svg><g>foo</g><g>bar</g></svg><p>baz</caption></table>",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true,
+            "caption": true,
+            "svg svg": true,
+            "svg g": true,
+            "p": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "caption",
+                        "children": [
+                          {
+                            "tag": "svg",
+                            "ns": "http://www.w3.org/2000/svg",
+                            "children": [
+                              {
+                                "tag": "g",
+                                "ns": "http://www.w3.org/2000/svg",
+                                "children": [
+                                  {
+                                    "text": "foo"
+                                  }
+                                ]
+                              },
+                              {
+                                "tag": "g",
+                                "ns": "http://www.w3.org/2000/svg",
+                                "children": [
+                                  {
+                                    "text": "bar"
+                                  }
+                                ]
+                              }
+                            ]
+                          },
+                          {
+                            "tag": "p",
+                            "children": [
+                              {
+                                "text": "baz"
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><table><caption><svg><g>foo</g><g>bar</g></svg><p>baz</p></caption></table></body></html>",
+        "noQuirksBodyHtml": "<table><caption><svg><g>foo</g><g>bar</g></svg><p>baz</p></caption></table>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><body><table><caption><svg><g>foo</g><g>bar</g><p>baz</table><p>quux",
+      "errors": [
+        "(1,65) unexpected-html-element-in-foreign-content"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true,
+            "caption": true,
+            "svg svg": true,
+            "svg g": true,
+            "p": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "caption",
+                        "children": [
+                          {
+                            "tag": "svg",
+                            "ns": "http://www.w3.org/2000/svg",
+                            "children": [
+                              {
+                                "tag": "g",
+                                "ns": "http://www.w3.org/2000/svg",
+                                "children": [
+                                  {
+                                    "text": "foo"
+                                  }
+                                ]
+                              },
+                              {
+                                "tag": "g",
+                                "ns": "http://www.w3.org/2000/svg",
+                                "children": [
+                                  {
+                                    "text": "bar"
+                                  }
+                                ]
+                              }
+                            ]
+                          },
+                          {
+                            "tag": "p",
+                            "children": [
+                              {
+                                "text": "baz"
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "p",
+                    "children": [
+                      {
+                        "text": "quux"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><table><caption><svg><g>foo</g><g>bar</g></svg><p>baz</p></caption></table><p>quux</p></body></html>",
+        "noQuirksBodyHtml": "<table><caption><svg><g>foo</g><g>bar</g><p>baz</p></svg></caption></table><p>quux</p>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><body><table><caption><svg><g>foo</g><g>bar</g>baz</table><p>quux",
+      "errors": [
+        "(1,73) unexpected-end-tag",
+        "(1,73) expected-one-end-tag-but-got-another"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true,
+            "caption": true,
+            "svg svg": true,
+            "svg g": true,
+            "p": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "caption",
+                        "children": [
+                          {
+                            "tag": "svg",
+                            "ns": "http://www.w3.org/2000/svg",
+                            "children": [
+                              {
+                                "tag": "g",
+                                "ns": "http://www.w3.org/2000/svg",
+                                "children": [
+                                  {
+                                    "text": "foo"
+                                  }
+                                ]
+                              },
+                              {
+                                "tag": "g",
+                                "ns": "http://www.w3.org/2000/svg",
+                                "children": [
+                                  {
+                                    "text": "bar"
+                                  }
+                                ]
+                              },
+                              {
+                                "text": "baz"
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "p",
+                    "children": [
+                      {
+                        "text": "quux"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><table><caption><svg><g>foo</g><g>bar</g>baz</svg></caption></table><p>quux</p></body></html>",
+        "noQuirksBodyHtml": "<table><caption><svg><g>foo</g><g>bar</g>baz</svg></caption></table><p>quux</p>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><body><table><colgroup><svg><g>foo</g><g>bar</g><p>baz</table><p>quux",
+      "errors": [
+        "(1,43) foster-parenting-start-tag svg",
+        "(1,66) unexpected HTML-like start tag token in foreign content",
+        "(1,66) foster-parenting-start-tag",
+        "(1,67) foster-parenting-character",
+        "(1,68) foster-parenting-character",
+        "(1,69) foster-parenting-character"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "svg svg": true,
+            "svg g": true,
+            "p": true,
+            "table": true,
+            "colgroup": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "svg",
+                    "ns": "http://www.w3.org/2000/svg",
+                    "children": [
+                      {
+                        "tag": "g",
+                        "ns": "http://www.w3.org/2000/svg",
+                        "children": [
+                          {
+                            "text": "foo"
+                          }
+                        ]
+                      },
+                      {
+                        "tag": "g",
+                        "ns": "http://www.w3.org/2000/svg",
+                        "children": [
+                          {
+                            "text": "bar"
+                          }
+                        ]
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "p",
+                    "children": [
+                      {
+                        "text": "baz"
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "colgroup"
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "p",
+                    "children": [
+                      {
+                        "text": "quux"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><svg><g>foo</g><g>bar</g></svg><p>baz</p><table><colgroup></colgroup></table><p>quux</p></body></html>",
+        "noQuirksBodyHtml": "<svg><g>foo</g><g>bar</g><p>baz</p></svg><table><colgroup></colgroup></table><p>quux</p>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><body><table><tr><td><select><svg><g>foo</g><g>bar</g><p>baz</table><p>quux",
+      "errors": [
+        "(1,49) unexpected-start-tag-in-select",
+        "(1,52) unexpected-start-tag-in-select",
+        "(1,59) unexpected-end-tag-in-select",
+        "(1,62) unexpected-start-tag-in-select",
+        "(1,69) unexpected-end-tag-in-select",
+        "(1,72) unexpected-start-tag-in-select",
+        "(1,83) unexpected-table-element-end-tag-in-select-in-table"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true,
+            "tbody": true,
+            "tr": true,
+            "td": true,
+            "select": true,
+            "p": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "tbody",
+                        "children": [
+                          {
+                            "tag": "tr",
+                            "children": [
+                              {
+                                "tag": "td",
+                                "children": [
+                                  {
+                                    "tag": "select",
+                                    "children": [
+                                      {
+                                        "text": "foobarbaz"
+                                      }
+                                    ]
+                                  }
+                                ]
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "p",
+                    "children": [
+                      {
+                        "text": "quux"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><table><tbody><tr><td><select>foobarbaz</select></td></tr></tbody></table><p>quux</p></body></html>",
+        "noQuirksBodyHtml": "<table><tbody><tr><td><select>foobarbaz</select></td></tr></tbody></table><p>quux</p>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><body><table><select><svg><g>foo</g><g>bar</g><p>baz</table><p>quux",
+      "errors": [
+        "(1,36) unexpected-start-tag-implies-table-voodoo",
+        "(1,41) unexpected-start-tag-in-select",
+        "(1,44) unexpected-start-tag-in-select",
+        "(1,51) unexpected-end-tag-in-select",
+        "(1,54) unexpected-start-tag-in-select",
+        "(1,61) unexpected-end-tag-in-select",
+        "(1,64) unexpected-start-tag-in-select",
+        "(1,75) unexpected-table-element-end-tag-in-select-in-table"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "select": true,
+            "table": true,
+            "p": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "select",
+                    "children": [
+                      {
+                        "text": "foobarbaz"
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "table"
+                  },
+                  {
+                    "tag": "p",
+                    "children": [
+                      {
+                        "text": "quux"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><select>foobarbaz</select><table></table><p>quux</p></body></html>",
+        "noQuirksBodyHtml": "<select>foobarbaz</select><table></table><p>quux</p>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><body></body></html><svg><g>foo</g><g>bar</g><p>baz",
+      "errors": [
+        "(1,40) expected-eof-but-got-start-tag",
+        "(1,63) unexpected-html-element-in-foreign-content"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "svg svg": true,
+            "svg g": true,
+            "p": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "svg",
+                    "ns": "http://www.w3.org/2000/svg",
+                    "children": [
+                      {
+                        "tag": "g",
+                        "ns": "http://www.w3.org/2000/svg",
+                        "children": [
+                          {
+                            "text": "foo"
+                          }
+                        ]
+                      },
+                      {
+                        "tag": "g",
+                        "ns": "http://www.w3.org/2000/svg",
+                        "children": [
+                          {
+                            "text": "bar"
+                          }
+                        ]
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "p",
+                    "children": [
+                      {
+                        "text": "baz"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><svg><g>foo</g><g>bar</g></svg><p>baz</p></body></html>",
+        "noQuirksBodyHtml": "<svg><g>foo</g><g>bar</g><p>baz</p></svg>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><body></body><svg><g>foo</g><g>bar</g><p>baz",
+      "errors": [
+        "(1,33) unexpected-start-tag-after-body",
+        "(1,56) unexpected-html-element-in-foreign-content"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "svg svg": true,
+            "svg g": true,
+            "p": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "svg",
+                    "ns": "http://www.w3.org/2000/svg",
+                    "children": [
+                      {
+                        "tag": "g",
+                        "ns": "http://www.w3.org/2000/svg",
+                        "children": [
+                          {
+                            "text": "foo"
+                          }
+                        ]
+                      },
+                      {
+                        "tag": "g",
+                        "ns": "http://www.w3.org/2000/svg",
+                        "children": [
+                          {
+                            "text": "bar"
+                          }
+                        ]
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "p",
+                    "children": [
+                      {
+                        "text": "baz"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><svg><g>foo</g><g>bar</g></svg><p>baz</p></body></html>",
+        "noQuirksBodyHtml": "<svg><g>foo</g><g>bar</g><p>baz</p></svg>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><frameset><svg><g></g><g></g><p><span>",
+      "errors": [
+        "(1,30) unexpected-start-tag-in-frameset",
+        "(1,33) unexpected-start-tag-in-frameset",
+        "(1,37) unexpected-end-tag-in-frameset",
+        "(1,40) unexpected-start-tag-in-frameset",
+        "(1,44) unexpected-end-tag-in-frameset",
+        "(1,47) unexpected-start-tag-in-frameset",
+        "(1,53) unexpected-start-tag-in-frameset",
+        "(1,53) eof-in-frameset"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "frameset": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "frameset"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><frameset></frameset></html>",
+        "noQuirksBodyHtml": "<svg><g></g><g></g><p><span></span></p></svg>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><frameset></frameset><svg><g></g><g></g><p><span>",
+      "errors": [
+        "(1,41) unexpected-start-tag-after-frameset",
+        "(1,44) unexpected-start-tag-after-frameset",
+        "(1,48) unexpected-end-tag-after-frameset",
+        "(1,51) unexpected-start-tag-after-frameset",
+        "(1,55) unexpected-end-tag-after-frameset",
+        "(1,58) unexpected-start-tag-after-frameset",
+        "(1,64) unexpected-start-tag-after-frameset"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "frameset": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "frameset"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><frameset></frameset></html>",
+        "noQuirksBodyHtml": "<svg><g></g><g></g><p><span></span></p></svg>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><body xlink:href=foo><svg xlink:href=foo></svg>",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "svg svg": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "attrs": [
+                  {
+                    "name": "xlink:href",
+                    "value": "foo"
+                  }
+                ],
+                "children": [
+                  {
+                    "tag": "svg",
+                    "ns": "http://www.w3.org/2000/svg",
+                    "attrs": [
+                      {
+                        "name": "href",
+                        "ns": "http://www.w3.org/1999/xlink",
+                        "value": "foo"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body xlink:href=\"foo\"><svg xlink:href=\"foo\"></svg></body></html>",
+        "noQuirksBodyHtml": "<svg xlink:href=\"foo\"></svg>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><body xlink:href=foo xml:lang=en><svg><g xml:lang=en xlink:href=foo></g></svg>",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "svg svg": true,
+            "svg g": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "attrs": [
+                  {
+                    "name": "xlink:href",
+                    "value": "foo"
+                  },
+                  {
+                    "name": "xml:lang",
+                    "value": "en"
+                  }
+                ],
+                "children": [
+                  {
+                    "tag": "svg",
+                    "ns": "http://www.w3.org/2000/svg",
+                    "children": [
+                      {
+                        "tag": "g",
+                        "ns": "http://www.w3.org/2000/svg",
+                        "attrs": [
+                          {
+                            "name": "href",
+                            "ns": "http://www.w3.org/1999/xlink",
+                            "value": "foo"
+                          },
+                          {
+                            "name": "lang",
+                            "ns": "http://www.w3.org/XML/1998/namespace",
+                            "value": "en"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body xlink:href=\"foo\" xml:lang=\"en\"><svg><g xml:lang=\"en\" xlink:href=\"foo\"></g></svg></body></html>",
+        "noQuirksBodyHtml": "<svg><g xml:lang=\"en\" xlink:href=\"foo\"></g></svg>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><body xlink:href=foo xml:lang=en><svg><g xml:lang=en xlink:href=foo /></svg>",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "svg svg": true,
+            "svg g": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "attrs": [
+                  {
+                    "name": "xlink:href",
+                    "value": "foo"
+                  },
+                  {
+                    "name": "xml:lang",
+                    "value": "en"
+                  }
+                ],
+                "children": [
+                  {
+                    "tag": "svg",
+                    "ns": "http://www.w3.org/2000/svg",
+                    "children": [
+                      {
+                        "tag": "g",
+                        "ns": "http://www.w3.org/2000/svg",
+                        "attrs": [
+                          {
+                            "name": "href",
+                            "ns": "http://www.w3.org/1999/xlink",
+                            "value": "foo"
+                          },
+                          {
+                            "name": "lang",
+                            "ns": "http://www.w3.org/XML/1998/namespace",
+                            "value": "en"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body xlink:href=\"foo\" xml:lang=\"en\"><svg><g xml:lang=\"en\" xlink:href=\"foo\"></g></svg></body></html>",
+        "noQuirksBodyHtml": "<svg><g xml:lang=\"en\" xlink:href=\"foo\"></g></svg>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><body xlink:href=foo xml:lang=en><svg><g xml:lang=en xlink:href=foo />bar</svg>",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "svg svg": true,
+            "svg g": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "attrs": [
+                  {
+                    "name": "xlink:href",
+                    "value": "foo"
+                  },
+                  {
+                    "name": "xml:lang",
+                    "value": "en"
+                  }
+                ],
+                "children": [
+                  {
+                    "tag": "svg",
+                    "ns": "http://www.w3.org/2000/svg",
+                    "children": [
+                      {
+                        "tag": "g",
+                        "ns": "http://www.w3.org/2000/svg",
+                        "attrs": [
+                          {
+                            "name": "href",
+                            "ns": "http://www.w3.org/1999/xlink",
+                            "value": "foo"
+                          },
+                          {
+                            "name": "lang",
+                            "ns": "http://www.w3.org/XML/1998/namespace",
+                            "value": "en"
+                          }
+                        ]
+                      },
+                      {
+                        "text": "bar"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body xlink:href=\"foo\" xml:lang=\"en\"><svg><g xml:lang=\"en\" xlink:href=\"foo\"></g>bar</svg></body></html>",
+        "noQuirksBodyHtml": "<svg><g xml:lang=\"en\" xlink:href=\"foo\"></g>bar</svg>"
+      }
+    },
+    {
+      "data": "<svg></path>",
+      "errors": [
+        "(1,5) expected-doctype-but-got-start-tag",
+        "(1,12) unexpected-end-tag",
+        "(1,12) unexpected-end-tag",
+        "(1,12) expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "svg svg": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "svg",
+                    "ns": "http://www.w3.org/2000/svg"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><svg></svg></body></html>",
+        "noQuirksBodyHtml": "<svg></svg>"
+      }
+    },
+    {
+      "data": "<div><svg></div>a",
+      "errors": [
+        "(1,5) expected-doctype-but-got-start-tag",
+        "(1,16) unexpected-end-tag",
+        "(1,16) end-tag-too-early"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "div": true,
+            "svg svg": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "div",
+                    "children": [
+                      {
+                        "tag": "svg",
+                        "ns": "http://www.w3.org/2000/svg"
+                      }
+                    ]
+                  },
+                  {
+                    "text": "a"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><div><svg></svg></div>a</body></html>",
+        "noQuirksBodyHtml": "<div><svg></svg></div>a"
+      }
+    },
+    {
+      "data": "<div><svg><path></div>a",
+      "errors": [
+        "(1,5) expected-doctype-but-got-start-tag",
+        "(1,22) unexpected-end-tag",
+        "(1,22) end-tag-too-early"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "div": true,
+            "svg svg": true,
+            "svg path": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "div",
+                    "children": [
+                      {
+                        "tag": "svg",
+                        "ns": "http://www.w3.org/2000/svg",
+                        "children": [
+                          {
+                            "tag": "path",
+                            "ns": "http://www.w3.org/2000/svg"
+                          }
+                        ]
+                      }
+                    ]
+                  },
+                  {
+                    "text": "a"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><div><svg><path></path></svg></div>a</body></html>",
+        "noQuirksBodyHtml": "<div><svg><path></path></svg></div>a"
+      }
+    },
+    {
+      "data": "<div><svg><path></svg><path>",
+      "errors": [
+        "(1,5) expected-doctype-but-got-start-tag",
+        "(1,22) unexpected-end-tag",
+        "(1,28) expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "div": true,
+            "svg svg": true,
+            "svg path": true,
+            "path": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "div",
+                    "children": [
+                      {
+                        "tag": "svg",
+                        "ns": "http://www.w3.org/2000/svg",
+                        "children": [
+                          {
+                            "tag": "path",
+                            "ns": "http://www.w3.org/2000/svg"
+                          }
+                        ]
+                      },
+                      {
+                        "tag": "path"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><div><svg><path></path></svg><path></path></div></body></html>",
+        "noQuirksBodyHtml": "<div><svg><path></path></svg><path></path></div>"
+      }
+    },
+    {
+      "data": "<div><svg><path><foreignObject><math></div>a",
+      "errors": [
+        "(1,5) expected-doctype-but-got-start-tag",
+        "(1,43) unexpected-end-tag",
+        "(1,43) end-tag-too-early",
+        "(1,44) expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "div": true,
+            "svg svg": true,
+            "svg path": true,
+            "svg foreignObject": true,
+            "math math": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "div",
+                    "children": [
+                      {
+                        "tag": "svg",
+                        "ns": "http://www.w3.org/2000/svg",
+                        "children": [
+                          {
+                            "tag": "path",
+                            "ns": "http://www.w3.org/2000/svg",
+                            "children": [
+                              {
+                                "tag": "foreignObject",
+                                "ns": "http://www.w3.org/2000/svg",
+                                "children": [
+                                  {
+                                    "tag": "math",
+                                    "ns": "http://www.w3.org/1998/Math/MathML",
+                                    "children": [
+                                      {
+                                        "text": "a"
+                                      }
+                                    ]
+                                  }
+                                ]
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><div><svg><path><foreignObject><math>a</math></foreignObject></path></svg></div></body></html>",
+        "noQuirksBodyHtml": "<div><svg><path><foreignObject><math>a</math></foreignObject></path></svg></div>"
+      }
+    },
+    {
+      "data": "<div><svg><path><foreignObject><p></div>a",
+      "errors": [
+        "(1,5) expected-doctype-but-got-start-tag",
+        "(1,40) end-tag-too-early",
+        "(1,41) expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "div": true,
+            "svg svg": true,
+            "svg path": true,
+            "svg foreignObject": true,
+            "p": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "div",
+                    "children": [
+                      {
+                        "tag": "svg",
+                        "ns": "http://www.w3.org/2000/svg",
+                        "children": [
+                          {
+                            "tag": "path",
+                            "ns": "http://www.w3.org/2000/svg",
+                            "children": [
+                              {
+                                "tag": "foreignObject",
+                                "ns": "http://www.w3.org/2000/svg",
+                                "children": [
+                                  {
+                                    "tag": "p",
+                                    "children": [
+                                      {
+                                        "text": "a"
+                                      }
+                                    ]
+                                  }
+                                ]
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><div><svg><path><foreignObject><p>a</p></foreignObject></path></svg></div></body></html>",
+        "noQuirksBodyHtml": "<div><svg><path><foreignObject><p>a</p></foreignObject></path></svg></div>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><svg><desc><div><svg><ul>a",
+      "errors": [
+        "(1,40) unexpected-html-element-in-foreign-content",
+        "(1,41) expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "svg svg": true,
+            "svg desc": true,
+            "div": true,
+            "ul": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "svg",
+                    "ns": "http://www.w3.org/2000/svg",
+                    "children": [
+                      {
+                        "tag": "desc",
+                        "ns": "http://www.w3.org/2000/svg",
+                        "children": [
+                          {
+                            "tag": "div",
+                            "children": [
+                              {
+                                "tag": "svg",
+                                "ns": "http://www.w3.org/2000/svg"
+                              },
+                              {
+                                "tag": "ul",
+                                "children": [
+                                  {
+                                    "text": "a"
+                                  }
+                                ]
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><svg><desc><div><svg></svg><ul>a</ul></div></desc></svg></body></html>",
+        "noQuirksBodyHtml": "<svg><desc><div><svg><ul>a</ul></svg></div></desc></svg>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><svg><desc><svg><ul>a",
+      "errors": [
+        "(1,35) unexpected-html-element-in-foreign-content",
+        "(1,36) expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "svg svg": true,
+            "svg desc": true,
+            "ul": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "svg",
+                    "ns": "http://www.w3.org/2000/svg",
+                    "children": [
+                      {
+                        "tag": "desc",
+                        "ns": "http://www.w3.org/2000/svg",
+                        "children": [
+                          {
+                            "tag": "svg",
+                            "ns": "http://www.w3.org/2000/svg"
+                          },
+                          {
+                            "tag": "ul",
+                            "children": [
+                              {
+                                "text": "a"
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><svg><desc><svg></svg><ul>a</ul></desc></svg></body></html>",
+        "noQuirksBodyHtml": "<svg><desc><svg><ul>a</ul></svg></desc></svg>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><p><svg><desc><p>",
+      "errors": [
+        "(1,32) expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "p": true,
+            "svg svg": true,
+            "svg desc": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "p",
+                    "children": [
+                      {
+                        "tag": "svg",
+                        "ns": "http://www.w3.org/2000/svg",
+                        "children": [
+                          {
+                            "tag": "desc",
+                            "ns": "http://www.w3.org/2000/svg",
+                            "children": [
+                              {
+                                "tag": "p"
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><p><svg><desc><p></p></desc></svg></p></body></html>",
+        "noQuirksBodyHtml": "<p><svg><desc><p></p></desc></svg></p>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><p><svg><title><p>",
+      "errors": [
+        "(1,33) expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "p": true,
+            "svg svg": true,
+            "svg title": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "p",
+                    "children": [
+                      {
+                        "tag": "svg",
+                        "ns": "http://www.w3.org/2000/svg",
+                        "children": [
+                          {
+                            "tag": "title",
+                            "ns": "http://www.w3.org/2000/svg",
+                            "children": [
+                              {
+                                "tag": "p"
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><p><svg><title><p></p></title></svg></p></body></html>",
+        "noQuirksBodyHtml": "<p><svg><title><p></p></title></svg></p>"
+      }
+    },
+    {
+      "data": "<div><svg><path><foreignObject><p></foreignObject><p>",
+      "errors": [
+        "(1,5) expected-doctype-but-got-start-tag",
+        "(1,50) unexpected-end-tag",
+        "(1,53) expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "div": true,
+            "svg svg": true,
+            "svg path": true,
+            "svg foreignObject": true,
+            "p": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "div",
+                    "children": [
+                      {
+                        "tag": "svg",
+                        "ns": "http://www.w3.org/2000/svg",
+                        "children": [
+                          {
+                            "tag": "path",
+                            "ns": "http://www.w3.org/2000/svg",
+                            "children": [
+                              {
+                                "tag": "foreignObject",
+                                "ns": "http://www.w3.org/2000/svg",
+                                "children": [
+                                  {
+                                    "tag": "p"
+                                  },
+                                  {
+                                    "tag": "p"
+                                  }
+                                ]
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><div><svg><path><foreignObject><p></p><p></p></foreignObject></path></svg></div></body></html>",
+        "noQuirksBodyHtml": "<div><svg><path><foreignObject><p></p><p></p></foreignObject></path></svg></div>"
+      }
+    },
+    {
+      "data": "<math><mi><div><object><div><span></span></div></object></div></mi><mi>",
+      "errors": [
+        "(1,6) expected-doctype-but-got-start-tag",
+        "(1,71) expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "math math": true,
+            "math mi": true,
+            "div": true,
+            "object": true,
+            "span": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "math",
+                    "ns": "http://www.w3.org/1998/Math/MathML",
+                    "children": [
+                      {
+                        "tag": "mi",
+                        "ns": "http://www.w3.org/1998/Math/MathML",
+                        "children": [
+                          {
+                            "tag": "div",
+                            "children": [
+                              {
+                                "tag": "object",
+                                "children": [
+                                  {
+                                    "tag": "div",
+                                    "children": [
+                                      {
+                                        "tag": "span"
+                                      }
+                                    ]
+                                  }
+                                ]
+                              }
+                            ]
+                          }
+                        ]
+                      },
+                      {
+                        "tag": "mi",
+                        "ns": "http://www.w3.org/1998/Math/MathML"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><math><mi><div><object><div><span></span></div></object></div></mi><mi></mi></math></body></html>",
+        "noQuirksBodyHtml": "<math><mi><div><object><div><span></span></div></object></div></mi><mi></mi></math>"
+      }
+    },
+    {
+      "data": "<math><mi><svg><foreignObject><div><div></div></div></foreignObject></svg></mi><mi>",
+      "errors": [
+        "(1,6) expected-doctype-but-got-start-tag",
+        "(1,83) expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "math math": true,
+            "math mi": true,
+            "svg svg": true,
+            "svg foreignObject": true,
+            "div": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "math",
+                    "ns": "http://www.w3.org/1998/Math/MathML",
+                    "children": [
+                      {
+                        "tag": "mi",
+                        "ns": "http://www.w3.org/1998/Math/MathML",
+                        "children": [
+                          {
+                            "tag": "svg",
+                            "ns": "http://www.w3.org/2000/svg",
+                            "children": [
+                              {
+                                "tag": "foreignObject",
+                                "ns": "http://www.w3.org/2000/svg",
+                                "children": [
+                                  {
+                                    "tag": "div",
+                                    "children": [
+                                      {
+                                        "tag": "div"
+                                      }
+                                    ]
+                                  }
+                                ]
+                              }
+                            ]
+                          }
+                        ]
+                      },
+                      {
+                        "tag": "mi",
+                        "ns": "http://www.w3.org/1998/Math/MathML"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><math><mi><svg><foreignObject><div><div></div></div></foreignObject></svg></mi><mi></mi></math></body></html>",
+        "noQuirksBodyHtml": "<math><mi><svg><foreignObject><div><div></div></div></foreignObject></svg></mi><mi></mi></math>"
+      }
+    },
+    {
+      "data": "<svg><script></script><path>",
+      "errors": [
+        "(1,5) expected-doctype-but-got-start-tag",
+        "(1,28) expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "svg svg": true,
+            "svg script": true,
+            "svg path": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "svg",
+                    "ns": "http://www.w3.org/2000/svg",
+                    "children": [
+                      {
+                        "tag": "script",
+                        "ns": "http://www.w3.org/2000/svg"
+                      },
+                      {
+                        "tag": "path",
+                        "ns": "http://www.w3.org/2000/svg"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><svg><script></script><path></path></svg></body></html>",
+        "noQuirksBodyHtml": "<svg><script></script><path></path></svg>"
+      }
+    },
+    {
+      "data": "<table><svg></svg><tr>",
+      "errors": [
+        "(1,7) expected-doctype-but-got-start-tag",
+        "(1,12) unexpected-start-tag-implies-table-voodoo",
+        "(1,22) eof-in-table"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "svg svg": true,
+            "table": true,
+            "tbody": true,
+            "tr": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "svg",
+                    "ns": "http://www.w3.org/2000/svg"
+                  },
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "tbody",
+                        "children": [
+                          {
+                            "tag": "tr"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><svg></svg><table><tbody><tr></tr></tbody></table></body></html>",
+        "noQuirksBodyHtml": "<svg></svg><table><tbody><tr></tr></tbody></table>"
+      }
+    },
+    {
+      "data": "<math><mi><mglyph>",
+      "errors": [
+        "(1,6) expected-doctype-but-got-start-tag",
+        "(1,18) expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "math math": true,
+            "math mi": true,
+            "math mglyph": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "math",
+                    "ns": "http://www.w3.org/1998/Math/MathML",
+                    "children": [
+                      {
+                        "tag": "mi",
+                        "ns": "http://www.w3.org/1998/Math/MathML",
+                        "children": [
+                          {
+                            "tag": "mglyph",
+                            "ns": "http://www.w3.org/1998/Math/MathML"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><math><mi><mglyph></mglyph></mi></math></body></html>",
+        "noQuirksBodyHtml": "<math><mi><mglyph></mglyph></mi></math>"
+      }
+    },
+    {
+      "data": "<math><mi><malignmark>",
+      "errors": [
+        "(1,6) expected-doctype-but-got-start-tag",
+        "(1,22) expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "math math": true,
+            "math mi": true,
+            "math malignmark": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "math",
+                    "ns": "http://www.w3.org/1998/Math/MathML",
+                    "children": [
+                      {
+                        "tag": "mi",
+                        "ns": "http://www.w3.org/1998/Math/MathML",
+                        "children": [
+                          {
+                            "tag": "malignmark",
+                            "ns": "http://www.w3.org/1998/Math/MathML"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><math><mi><malignmark></malignmark></mi></math></body></html>",
+        "noQuirksBodyHtml": "<math><mi><malignmark></malignmark></mi></math>"
+      }
+    },
+    {
+      "data": "<math><mo><mglyph>",
+      "errors": [
+        "(1,6) expected-doctype-but-got-start-tag",
+        "(1,18) expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "math math": true,
+            "math mo": true,
+            "math mglyph": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "math",
+                    "ns": "http://www.w3.org/1998/Math/MathML",
+                    "children": [
+                      {
+                        "tag": "mo",
+                        "ns": "http://www.w3.org/1998/Math/MathML",
+                        "children": [
+                          {
+                            "tag": "mglyph",
+                            "ns": "http://www.w3.org/1998/Math/MathML"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><math><mo><mglyph></mglyph></mo></math></body></html>",
+        "noQuirksBodyHtml": "<math><mo><mglyph></mglyph></mo></math>"
+      }
+    },
+    {
+      "data": "<math><mo><malignmark>",
+      "errors": [
+        "(1,6) expected-doctype-but-got-start-tag",
+        "(1,22) expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "math math": true,
+            "math mo": true,
+            "math malignmark": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "math",
+                    "ns": "http://www.w3.org/1998/Math/MathML",
+                    "children": [
+                      {
+                        "tag": "mo",
+                        "ns": "http://www.w3.org/1998/Math/MathML",
+                        "children": [
+                          {
+                            "tag": "malignmark",
+                            "ns": "http://www.w3.org/1998/Math/MathML"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><math><mo><malignmark></malignmark></mo></math></body></html>",
+        "noQuirksBodyHtml": "<math><mo><malignmark></malignmark></mo></math>"
+      }
+    },
+    {
+      "data": "<math><mn><mglyph>",
+      "errors": [
+        "(1,6) expected-doctype-but-got-start-tag",
+        "(1,18) expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "math math": true,
+            "math mn": true,
+            "math mglyph": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "math",
+                    "ns": "http://www.w3.org/1998/Math/MathML",
+                    "children": [
+                      {
+                        "tag": "mn",
+                        "ns": "http://www.w3.org/1998/Math/MathML",
+                        "children": [
+                          {
+                            "tag": "mglyph",
+                            "ns": "http://www.w3.org/1998/Math/MathML"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><math><mn><mglyph></mglyph></mn></math></body></html>",
+        "noQuirksBodyHtml": "<math><mn><mglyph></mglyph></mn></math>"
+      }
+    },
+    {
+      "data": "<math><mn><malignmark>",
+      "errors": [
+        "(1,6) expected-doctype-but-got-start-tag",
+        "(1,22) expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "math math": true,
+            "math mn": true,
+            "math malignmark": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "math",
+                    "ns": "http://www.w3.org/1998/Math/MathML",
+                    "children": [
+                      {
+                        "tag": "mn",
+                        "ns": "http://www.w3.org/1998/Math/MathML",
+                        "children": [
+                          {
+                            "tag": "malignmark",
+                            "ns": "http://www.w3.org/1998/Math/MathML"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><math><mn><malignmark></malignmark></mn></math></body></html>",
+        "noQuirksBodyHtml": "<math><mn><malignmark></malignmark></mn></math>"
+      }
+    },
+    {
+      "data": "<math><ms><mglyph>",
+      "errors": [
+        "(1,6) expected-doctype-but-got-start-tag",
+        "(1,18) expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "math math": true,
+            "math ms": true,
+            "math mglyph": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "math",
+                    "ns": "http://www.w3.org/1998/Math/MathML",
+                    "children": [
+                      {
+                        "tag": "ms",
+                        "ns": "http://www.w3.org/1998/Math/MathML",
+                        "children": [
+                          {
+                            "tag": "mglyph",
+                            "ns": "http://www.w3.org/1998/Math/MathML"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><math><ms><mglyph></mglyph></ms></math></body></html>",
+        "noQuirksBodyHtml": "<math><ms><mglyph></mglyph></ms></math>"
+      }
+    },
+    {
+      "data": "<math><ms><malignmark>",
+      "errors": [
+        "(1,6) expected-doctype-but-got-start-tag",
+        "(1,22) expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "math math": true,
+            "math ms": true,
+            "math malignmark": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "math",
+                    "ns": "http://www.w3.org/1998/Math/MathML",
+                    "children": [
+                      {
+                        "tag": "ms",
+                        "ns": "http://www.w3.org/1998/Math/MathML",
+                        "children": [
+                          {
+                            "tag": "malignmark",
+                            "ns": "http://www.w3.org/1998/Math/MathML"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><math><ms><malignmark></malignmark></ms></math></body></html>",
+        "noQuirksBodyHtml": "<math><ms><malignmark></malignmark></ms></math>"
+      }
+    },
+    {
+      "data": "<math><mtext><mglyph>",
+      "errors": [
+        "(1,6) expected-doctype-but-got-start-tag",
+        "(1,21) expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "math math": true,
+            "math mtext": true,
+            "math mglyph": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "math",
+                    "ns": "http://www.w3.org/1998/Math/MathML",
+                    "children": [
+                      {
+                        "tag": "mtext",
+                        "ns": "http://www.w3.org/1998/Math/MathML",
+                        "children": [
+                          {
+                            "tag": "mglyph",
+                            "ns": "http://www.w3.org/1998/Math/MathML"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><math><mtext><mglyph></mglyph></mtext></math></body></html>",
+        "noQuirksBodyHtml": "<math><mtext><mglyph></mglyph></mtext></math>"
+      }
+    },
+    {
+      "data": "<math><mtext><malignmark>",
+      "errors": [
+        "(1,6) expected-doctype-but-got-start-tag",
+        "(1,25) expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "math math": true,
+            "math mtext": true,
+            "math malignmark": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "math",
+                    "ns": "http://www.w3.org/1998/Math/MathML",
+                    "children": [
+                      {
+                        "tag": "mtext",
+                        "ns": "http://www.w3.org/1998/Math/MathML",
+                        "children": [
+                          {
+                            "tag": "malignmark",
+                            "ns": "http://www.w3.org/1998/Math/MathML"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><math><mtext><malignmark></malignmark></mtext></math></body></html>",
+        "noQuirksBodyHtml": "<math><mtext><malignmark></malignmark></mtext></math>"
+      }
+    },
+    {
+      "data": "<math><annotation-xml><svg></svg></annotation-xml><mi>",
+      "errors": [
+        "(1,6) expected-doctype-but-got-start-tag",
+        "(1,54) expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "math math": true,
+            "math annotation-xml": true,
+            "svg svg": true,
+            "math mi": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "math",
+                    "ns": "http://www.w3.org/1998/Math/MathML",
+                    "children": [
+                      {
+                        "tag": "annotation-xml",
+                        "ns": "http://www.w3.org/1998/Math/MathML",
+                        "children": [
+                          {
+                            "tag": "svg",
+                            "ns": "http://www.w3.org/2000/svg"
+                          }
+                        ]
+                      },
+                      {
+                        "tag": "mi",
+                        "ns": "http://www.w3.org/1998/Math/MathML"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><math><annotation-xml><svg></svg></annotation-xml><mi></mi></math></body></html>",
+        "noQuirksBodyHtml": "<math><annotation-xml><svg></svg></annotation-xml><mi></mi></math>"
+      }
+    },
+    {
+      "data": "<math><annotation-xml><svg><foreignObject><div><math><mi></mi></math><span></span></div></foreignObject><path></path></svg></annotation-xml><mi>",
+      "errors": [
+        "(1,6) expected-doctype-but-got-start-tag",
+        "(1,144) expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "math math": true,
+            "math annotation-xml": true,
+            "svg svg": true,
+            "svg foreignObject": true,
+            "div": true,
+            "math mi": true,
+            "span": true,
+            "svg path": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "math",
+                    "ns": "http://www.w3.org/1998/Math/MathML",
+                    "children": [
+                      {
+                        "tag": "annotation-xml",
+                        "ns": "http://www.w3.org/1998/Math/MathML",
+                        "children": [
+                          {
+                            "tag": "svg",
+                            "ns": "http://www.w3.org/2000/svg",
+                            "children": [
+                              {
+                                "tag": "foreignObject",
+                                "ns": "http://www.w3.org/2000/svg",
+                                "children": [
+                                  {
+                                    "tag": "div",
+                                    "children": [
+                                      {
+                                        "tag": "math",
+                                        "ns": "http://www.w3.org/1998/Math/MathML",
+                                        "children": [
+                                          {
+                                            "tag": "mi",
+                                            "ns": "http://www.w3.org/1998/Math/MathML"
+                                          }
+                                        ]
+                                      },
+                                      {
+                                        "tag": "span"
+                                      }
+                                    ]
+                                  }
+                                ]
+                              },
+                              {
+                                "tag": "path",
+                                "ns": "http://www.w3.org/2000/svg"
+                              }
+                            ]
+                          }
+                        ]
+                      },
+                      {
+                        "tag": "mi",
+                        "ns": "http://www.w3.org/1998/Math/MathML"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><math><annotation-xml><svg><foreignObject><div><math><mi></mi></math><span></span></div></foreignObject><path></path></svg></annotation-xml><mi></mi></math></body></html>",
+        "noQuirksBodyHtml": "<math><annotation-xml><svg><foreignObject><div><math><mi></mi></math><span></span></div></foreignObject><path></path></svg></annotation-xml><mi></mi></math>"
+      }
+    },
+    {
+      "data": "<math><annotation-xml><svg><foreignObject><math><mi><svg></svg></mi><mo></mo></math><span></span></foreignObject><path></path></svg></annotation-xml><mi>",
+      "errors": [
+        "(1,6) expected-doctype-but-got-start-tag",
+        "(1,153) expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "math math": true,
+            "math annotation-xml": true,
+            "svg svg": true,
+            "svg foreignObject": true,
+            "math mi": true,
+            "math mo": true,
+            "span": true,
+            "svg path": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "math",
+                    "ns": "http://www.w3.org/1998/Math/MathML",
+                    "children": [
+                      {
+                        "tag": "annotation-xml",
+                        "ns": "http://www.w3.org/1998/Math/MathML",
+                        "children": [
+                          {
+                            "tag": "svg",
+                            "ns": "http://www.w3.org/2000/svg",
+                            "children": [
+                              {
+                                "tag": "foreignObject",
+                                "ns": "http://www.w3.org/2000/svg",
+                                "children": [
+                                  {
+                                    "tag": "math",
+                                    "ns": "http://www.w3.org/1998/Math/MathML",
+                                    "children": [
+                                      {
+                                        "tag": "mi",
+                                        "ns": "http://www.w3.org/1998/Math/MathML",
+                                        "children": [
+                                          {
+                                            "tag": "svg",
+                                            "ns": "http://www.w3.org/2000/svg"
+                                          }
+                                        ]
+                                      },
+                                      {
+                                        "tag": "mo",
+                                        "ns": "http://www.w3.org/1998/Math/MathML"
+                                      }
+                                    ]
+                                  },
+                                  {
+                                    "tag": "span"
+                                  }
+                                ]
+                              },
+                              {
+                                "tag": "path",
+                                "ns": "http://www.w3.org/2000/svg"
+                              }
+                            ]
+                          }
+                        ]
+                      },
+                      {
+                        "tag": "mi",
+                        "ns": "http://www.w3.org/1998/Math/MathML"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><math><annotation-xml><svg><foreignObject><math><mi><svg></svg></mi><mo></mo></math><span></span></foreignObject><path></path></svg></annotation-xml><mi></mi></math></body></html>",
+        "noQuirksBodyHtml": "<math><annotation-xml><svg><foreignObject><math><mi><svg></svg></mi><mo></mo></math><span></span></foreignObject><path></path></svg></annotation-xml><mi></mi></math>"
+      }
+    }
+  ],
+  "tests11.dat": [
+    {
+      "data": "<!DOCTYPE html><body><svg attributeName='' attributeType='' baseFrequency='' baseProfile='' calcMode='' clipPathUnits='' diffuseConstant='' edgeMode='' filterUnits='' glyphRef='' gradientTransform='' gradientUnits='' kernelMatrix='' kernelUnitLength='' keyPoints='' keySplines='' keyTimes='' lengthAdjust='' limitingConeAngle='' markerHeight='' markerUnits='' markerWidth='' maskContentUnits='' maskUnits='' numOctaves='' pathLength='' patternContentUnits='' patternTransform='' patternUnits='' pointsAtX='' pointsAtY='' pointsAtZ='' preserveAlpha='' preserveAspectRatio='' primitiveUnits='' refX='' refY='' repeatCount='' repeatDur='' requiredExtensions='' requiredFeatures='' specularConstant='' specularExponent='' spreadMethod='' startOffset='' stdDeviation='' stitchTiles='' surfaceScale='' systemLanguage='' tableValues='' targetX='' targetY='' textLength='' viewBox='' viewTarget='' xChannelSelector='' yChannelSelector='' zoomAndPan=''></svg>",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "svg svg": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "svg",
+                    "ns": "http://www.w3.org/2000/svg",
+                    "attrs": [
+                      {
+                        "name": "attributeName",
+                        "value": ""
+                      },
+                      {
+                        "name": "attributeType",
+                        "value": ""
+                      },
+                      {
+                        "name": "baseFrequency",
+                        "value": ""
+                      },
+                      {
+                        "name": "baseProfile",
+                        "value": ""
+                      },
+                      {
+                        "name": "calcMode",
+                        "value": ""
+                      },
+                      {
+                        "name": "clipPathUnits",
+                        "value": ""
+                      },
+                      {
+                        "name": "diffuseConstant",
+                        "value": ""
+                      },
+                      {
+                        "name": "edgeMode",
+                        "value": ""
+                      },
+                      {
+                        "name": "filterUnits",
+                        "value": ""
+                      },
+                      {
+                        "name": "glyphRef",
+                        "value": ""
+                      },
+                      {
+                        "name": "gradientTransform",
+                        "value": ""
+                      },
+                      {
+                        "name": "gradientUnits",
+                        "value": ""
+                      },
+                      {
+                        "name": "kernelMatrix",
+                        "value": ""
+                      },
+                      {
+                        "name": "kernelUnitLength",
+                        "value": ""
+                      },
+                      {
+                        "name": "keyPoints",
+                        "value": ""
+                      },
+                      {
+                        "name": "keySplines",
+                        "value": ""
+                      },
+                      {
+                        "name": "keyTimes",
+                        "value": ""
+                      },
+                      {
+                        "name": "lengthAdjust",
+                        "value": ""
+                      },
+                      {
+                        "name": "limitingConeAngle",
+                        "value": ""
+                      },
+                      {
+                        "name": "markerHeight",
+                        "value": ""
+                      },
+                      {
+                        "name": "markerUnits",
+                        "value": ""
+                      },
+                      {
+                        "name": "markerWidth",
+                        "value": ""
+                      },
+                      {
+                        "name": "maskContentUnits",
+                        "value": ""
+                      },
+                      {
+                        "name": "maskUnits",
+                        "value": ""
+                      },
+                      {
+                        "name": "numOctaves",
+                        "value": ""
+                      },
+                      {
+                        "name": "pathLength",
+                        "value": ""
+                      },
+                      {
+                        "name": "patternContentUnits",
+                        "value": ""
+                      },
+                      {
+                        "name": "patternTransform",
+                        "value": ""
+                      },
+                      {
+                        "name": "patternUnits",
+                        "value": ""
+                      },
+                      {
+                        "name": "pointsAtX",
+                        "value": ""
+                      },
+                      {
+                        "name": "pointsAtY",
+                        "value": ""
+                      },
+                      {
+                        "name": "pointsAtZ",
+                        "value": ""
+                      },
+                      {
+                        "name": "preserveAlpha",
+                        "value": ""
+                      },
+                      {
+                        "name": "preserveAspectRatio",
+                        "value": ""
+                      },
+                      {
+                        "name": "primitiveUnits",
+                        "value": ""
+                      },
+                      {
+                        "name": "refX",
+                        "value": ""
+                      },
+                      {
+                        "name": "refY",
+                        "value": ""
+                      },
+                      {
+                        "name": "repeatCount",
+                        "value": ""
+                      },
+                      {
+                        "name": "repeatDur",
+                        "value": ""
+                      },
+                      {
+                        "name": "requiredExtensions",
+                        "value": ""
+                      },
+                      {
+                        "name": "requiredFeatures",
+                        "value": ""
+                      },
+                      {
+                        "name": "specularConstant",
+                        "value": ""
+                      },
+                      {
+                        "name": "specularExponent",
+                        "value": ""
+                      },
+                      {
+                        "name": "spreadMethod",
+                        "value": ""
+                      },
+                      {
+                        "name": "startOffset",
+                        "value": ""
+                      },
+                      {
+                        "name": "stdDeviation",
+                        "value": ""
+                      },
+                      {
+                        "name": "stitchTiles",
+                        "value": ""
+                      },
+                      {
+                        "name": "surfaceScale",
+                        "value": ""
+                      },
+                      {
+                        "name": "systemLanguage",
+                        "value": ""
+                      },
+                      {
+                        "name": "tableValues",
+                        "value": ""
+                      },
+                      {
+                        "name": "targetX",
+                        "value": ""
+                      },
+                      {
+                        "name": "targetY",
+                        "value": ""
+                      },
+                      {
+                        "name": "textLength",
+                        "value": ""
+                      },
+                      {
+                        "name": "viewBox",
+                        "value": ""
+                      },
+                      {
+                        "name": "viewTarget",
+                        "value": ""
+                      },
+                      {
+                        "name": "xChannelSelector",
+                        "value": ""
+                      },
+                      {
+                        "name": "yChannelSelector",
+                        "value": ""
+                      },
+                      {
+                        "name": "zoomAndPan",
+                        "value": ""
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><svg attributeName=\"\" attributeType=\"\" baseFrequency=\"\" baseProfile=\"\" calcMode=\"\" clipPathUnits=\"\" diffuseConstant=\"\" edgeMode=\"\" filterUnits=\"\" glyphRef=\"\" gradientTransform=\"\" gradientUnits=\"\" kernelMatrix=\"\" kernelUnitLength=\"\" keyPoints=\"\" keySplines=\"\" keyTimes=\"\" lengthAdjust=\"\" limitingConeAngle=\"\" markerHeight=\"\" markerUnits=\"\" markerWidth=\"\" maskContentUnits=\"\" maskUnits=\"\" numOctaves=\"\" pathLength=\"\" patternContentUnits=\"\" patternTransform=\"\" patternUnits=\"\" pointsAtX=\"\" pointsAtY=\"\" pointsAtZ=\"\" preserveAlpha=\"\" preserveAspectRatio=\"\" primitiveUnits=\"\" refX=\"\" refY=\"\" repeatCount=\"\" repeatDur=\"\" requiredExtensions=\"\" requiredFeatures=\"\" specularConstant=\"\" specularExponent=\"\" spreadMethod=\"\" startOffset=\"\" stdDeviation=\"\" stitchTiles=\"\" surfaceScale=\"\" systemLanguage=\"\" tableValues=\"\" targetX=\"\" targetY=\"\" textLength=\"\" viewBox=\"\" viewTarget=\"\" xChannelSelector=\"\" yChannelSelector=\"\" zoomAndPan=\"\"></svg></body></html>",
+        "noQuirksBodyHtml": "<svg attributeName=\"\" attributeType=\"\" baseFrequency=\"\" baseProfile=\"\" calcMode=\"\" clipPathUnits=\"\" diffuseConstant=\"\" edgeMode=\"\" filterUnits=\"\" glyphRef=\"\" gradientTransform=\"\" gradientUnits=\"\" kernelMatrix=\"\" kernelUnitLength=\"\" keyPoints=\"\" keySplines=\"\" keyTimes=\"\" lengthAdjust=\"\" limitingConeAngle=\"\" markerHeight=\"\" markerUnits=\"\" markerWidth=\"\" maskContentUnits=\"\" maskUnits=\"\" numOctaves=\"\" pathLength=\"\" patternContentUnits=\"\" patternTransform=\"\" patternUnits=\"\" pointsAtX=\"\" pointsAtY=\"\" pointsAtZ=\"\" preserveAlpha=\"\" preserveAspectRatio=\"\" primitiveUnits=\"\" refX=\"\" refY=\"\" repeatCount=\"\" repeatDur=\"\" requiredExtensions=\"\" requiredFeatures=\"\" specularConstant=\"\" specularExponent=\"\" spreadMethod=\"\" startOffset=\"\" stdDeviation=\"\" stitchTiles=\"\" surfaceScale=\"\" systemLanguage=\"\" tableValues=\"\" targetX=\"\" targetY=\"\" textLength=\"\" viewBox=\"\" viewTarget=\"\" xChannelSelector=\"\" yChannelSelector=\"\" zoomAndPan=\"\"></svg>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><BODY><SVG ATTRIBUTENAME='' ATTRIBUTETYPE='' BASEFREQUENCY='' BASEPROFILE='' CALCMODE='' CLIPPATHUNITS='' DIFFUSECONSTANT='' EDGEMODE='' FILTERUNITS='' GLYPHREF='' GRADIENTTRANSFORM='' GRADIENTUNITS='' KERNELMATRIX='' KERNELUNITLENGTH='' KEYPOINTS='' KEYSPLINES='' KEYTIMES='' LENGTHADJUST='' LIMITINGCONEANGLE='' MARKERHEIGHT='' MARKERUNITS='' MARKERWIDTH='' MASKCONTENTUNITS='' MASKUNITS='' NUMOCTAVES='' PATHLENGTH='' PATTERNCONTENTUNITS='' PATTERNTRANSFORM='' PATTERNUNITS='' POINTSATX='' POINTSATY='' POINTSATZ='' PRESERVEALPHA='' PRESERVEASPECTRATIO='' PRIMITIVEUNITS='' REFX='' REFY='' REPEATCOUNT='' REPEATDUR='' REQUIREDEXTENSIONS='' REQUIREDFEATURES='' SPECULARCONSTANT='' SPECULAREXPONENT='' SPREADMETHOD='' STARTOFFSET='' STDDEVIATION='' STITCHTILES='' SURFACESCALE='' SYSTEMLANGUAGE='' TABLEVALUES='' TARGETX='' TARGETY='' TEXTLENGTH='' VIEWBOX='' VIEWTARGET='' XCHANNELSELECTOR='' YCHANNELSELECTOR='' ZOOMANDPAN=''></SVG>",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "svg svg": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "svg",
+                    "ns": "http://www.w3.org/2000/svg",
+                    "attrs": [
+                      {
+                        "name": "attributeName",
+                        "value": ""
+                      },
+                      {
+                        "name": "attributeType",
+                        "value": ""
+                      },
+                      {
+                        "name": "baseFrequency",
+                        "value": ""
+                      },
+                      {
+                        "name": "baseProfile",
+                        "value": ""
+                      },
+                      {
+                        "name": "calcMode",
+                        "value": ""
+                      },
+                      {
+                        "name": "clipPathUnits",
+                        "value": ""
+                      },
+                      {
+                        "name": "diffuseConstant",
+                        "value": ""
+                      },
+                      {
+                        "name": "edgeMode",
+                        "value": ""
+                      },
+                      {
+                        "name": "filterUnits",
+                        "value": ""
+                      },
+                      {
+                        "name": "glyphRef",
+                        "value": ""
+                      },
+                      {
+                        "name": "gradientTransform",
+                        "value": ""
+                      },
+                      {
+                        "name": "gradientUnits",
+                        "value": ""
+                      },
+                      {
+                        "name": "kernelMatrix",
+                        "value": ""
+                      },
+                      {
+                        "name": "kernelUnitLength",
+                        "value": ""
+                      },
+                      {
+                        "name": "keyPoints",
+                        "value": ""
+                      },
+                      {
+                        "name": "keySplines",
+                        "value": ""
+                      },
+                      {
+                        "name": "keyTimes",
+                        "value": ""
+                      },
+                      {
+                        "name": "lengthAdjust",
+                        "value": ""
+                      },
+                      {
+                        "name": "limitingConeAngle",
+                        "value": ""
+                      },
+                      {
+                        "name": "markerHeight",
+                        "value": ""
+                      },
+                      {
+                        "name": "markerUnits",
+                        "value": ""
+                      },
+                      {
+                        "name": "markerWidth",
+                        "value": ""
+                      },
+                      {
+                        "name": "maskContentUnits",
+                        "value": ""
+                      },
+                      {
+                        "name": "maskUnits",
+                        "value": ""
+                      },
+                      {
+                        "name": "numOctaves",
+                        "value": ""
+                      },
+                      {
+                        "name": "pathLength",
+                        "value": ""
+                      },
+                      {
+                        "name": "patternContentUnits",
+                        "value": ""
+                      },
+                      {
+                        "name": "patternTransform",
+                        "value": ""
+                      },
+                      {
+                        "name": "patternUnits",
+                        "value": ""
+                      },
+                      {
+                        "name": "pointsAtX",
+                        "value": ""
+                      },
+                      {
+                        "name": "pointsAtY",
+                        "value": ""
+                      },
+                      {
+                        "name": "pointsAtZ",
+                        "value": ""
+                      },
+                      {
+                        "name": "preserveAlpha",
+                        "value": ""
+                      },
+                      {
+                        "name": "preserveAspectRatio",
+                        "value": ""
+                      },
+                      {
+                        "name": "primitiveUnits",
+                        "value": ""
+                      },
+                      {
+                        "name": "refX",
+                        "value": ""
+                      },
+                      {
+                        "name": "refY",
+                        "value": ""
+                      },
+                      {
+                        "name": "repeatCount",
+                        "value": ""
+                      },
+                      {
+                        "name": "repeatDur",
+                        "value": ""
+                      },
+                      {
+                        "name": "requiredExtensions",
+                        "value": ""
+                      },
+                      {
+                        "name": "requiredFeatures",
+                        "value": ""
+                      },
+                      {
+                        "name": "specularConstant",
+                        "value": ""
+                      },
+                      {
+                        "name": "specularExponent",
+                        "value": ""
+                      },
+                      {
+                        "name": "spreadMethod",
+                        "value": ""
+                      },
+                      {
+                        "name": "startOffset",
+                        "value": ""
+                      },
+                      {
+                        "name": "stdDeviation",
+                        "value": ""
+                      },
+                      {
+                        "name": "stitchTiles",
+                        "value": ""
+                      },
+                      {
+                        "name": "surfaceScale",
+                        "value": ""
+                      },
+                      {
+                        "name": "systemLanguage",
+                        "value": ""
+                      },
+                      {
+                        "name": "tableValues",
+                        "value": ""
+                      },
+                      {
+                        "name": "targetX",
+                        "value": ""
+                      },
+                      {
+                        "name": "targetY",
+                        "value": ""
+                      },
+                      {
+                        "name": "textLength",
+                        "value": ""
+                      },
+                      {
+                        "name": "viewBox",
+                        "value": ""
+                      },
+                      {
+                        "name": "viewTarget",
+                        "value": ""
+                      },
+                      {
+                        "name": "xChannelSelector",
+                        "value": ""
+                      },
+                      {
+                        "name": "yChannelSelector",
+                        "value": ""
+                      },
+                      {
+                        "name": "zoomAndPan",
+                        "value": ""
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><svg attributeName=\"\" attributeType=\"\" baseFrequency=\"\" baseProfile=\"\" calcMode=\"\" clipPathUnits=\"\" diffuseConstant=\"\" edgeMode=\"\" filterUnits=\"\" glyphRef=\"\" gradientTransform=\"\" gradientUnits=\"\" kernelMatrix=\"\" kernelUnitLength=\"\" keyPoints=\"\" keySplines=\"\" keyTimes=\"\" lengthAdjust=\"\" limitingConeAngle=\"\" markerHeight=\"\" markerUnits=\"\" markerWidth=\"\" maskContentUnits=\"\" maskUnits=\"\" numOctaves=\"\" pathLength=\"\" patternContentUnits=\"\" patternTransform=\"\" patternUnits=\"\" pointsAtX=\"\" pointsAtY=\"\" pointsAtZ=\"\" preserveAlpha=\"\" preserveAspectRatio=\"\" primitiveUnits=\"\" refX=\"\" refY=\"\" repeatCount=\"\" repeatDur=\"\" requiredExtensions=\"\" requiredFeatures=\"\" specularConstant=\"\" specularExponent=\"\" spreadMethod=\"\" startOffset=\"\" stdDeviation=\"\" stitchTiles=\"\" surfaceScale=\"\" systemLanguage=\"\" tableValues=\"\" targetX=\"\" targetY=\"\" textLength=\"\" viewBox=\"\" viewTarget=\"\" xChannelSelector=\"\" yChannelSelector=\"\" zoomAndPan=\"\"></svg></body></html>",
+        "noQuirksBodyHtml": "<svg attributeName=\"\" attributeType=\"\" baseFrequency=\"\" baseProfile=\"\" calcMode=\"\" clipPathUnits=\"\" diffuseConstant=\"\" edgeMode=\"\" filterUnits=\"\" glyphRef=\"\" gradientTransform=\"\" gradientUnits=\"\" kernelMatrix=\"\" kernelUnitLength=\"\" keyPoints=\"\" keySplines=\"\" keyTimes=\"\" lengthAdjust=\"\" limitingConeAngle=\"\" markerHeight=\"\" markerUnits=\"\" markerWidth=\"\" maskContentUnits=\"\" maskUnits=\"\" numOctaves=\"\" pathLength=\"\" patternContentUnits=\"\" patternTransform=\"\" patternUnits=\"\" pointsAtX=\"\" pointsAtY=\"\" pointsAtZ=\"\" preserveAlpha=\"\" preserveAspectRatio=\"\" primitiveUnits=\"\" refX=\"\" refY=\"\" repeatCount=\"\" repeatDur=\"\" requiredExtensions=\"\" requiredFeatures=\"\" specularConstant=\"\" specularExponent=\"\" spreadMethod=\"\" startOffset=\"\" stdDeviation=\"\" stitchTiles=\"\" surfaceScale=\"\" systemLanguage=\"\" tableValues=\"\" targetX=\"\" targetY=\"\" textLength=\"\" viewBox=\"\" viewTarget=\"\" xChannelSelector=\"\" yChannelSelector=\"\" zoomAndPan=\"\"></svg>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><body><svg attributename='' attributetype='' basefrequency='' baseprofile='' calcmode='' clippathunits='' diffuseconstant='' edgemode='' filterunits='' filterres='' glyphref='' gradienttransform='' gradientunits='' kernelmatrix='' kernelunitlength='' keypoints='' keysplines='' keytimes='' lengthadjust='' limitingconeangle='' markerheight='' markerunits='' markerwidth='' maskcontentunits='' maskunits='' numoctaves='' pathlength='' patterncontentunits='' patterntransform='' patternunits='' pointsatx='' pointsaty='' pointsatz='' preservealpha='' preserveaspectratio='' primitiveunits='' refx='' refy='' repeatcount='' repeatdur='' requiredextensions='' requiredfeatures='' specularconstant='' specularexponent='' spreadmethod='' startoffset='' stddeviation='' stitchtiles='' surfacescale='' systemlanguage='' tablevalues='' targetx='' targety='' textlength='' viewbox='' viewtarget='' xchannelselector='' ychannelselector='' zoomandpan=''></svg>",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "svg svg": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "svg",
+                    "ns": "http://www.w3.org/2000/svg",
+                    "attrs": [
+                      {
+                        "name": "attributeName",
+                        "value": ""
+                      },
+                      {
+                        "name": "attributeType",
+                        "value": ""
+                      },
+                      {
+                        "name": "baseFrequency",
+                        "value": ""
+                      },
+                      {
+                        "name": "baseProfile",
+                        "value": ""
+                      },
+                      {
+                        "name": "calcMode",
+                        "value": ""
+                      },
+                      {
+                        "name": "clipPathUnits",
+                        "value": ""
+                      },
+                      {
+                        "name": "diffuseConstant",
+                        "value": ""
+                      },
+                      {
+                        "name": "edgeMode",
+                        "value": ""
+                      },
+                      {
+                        "name": "filterUnits",
+                        "value": ""
+                      },
+                      {
+                        "name": "filterres",
+                        "value": ""
+                      },
+                      {
+                        "name": "glyphRef",
+                        "value": ""
+                      },
+                      {
+                        "name": "gradientTransform",
+                        "value": ""
+                      },
+                      {
+                        "name": "gradientUnits",
+                        "value": ""
+                      },
+                      {
+                        "name": "kernelMatrix",
+                        "value": ""
+                      },
+                      {
+                        "name": "kernelUnitLength",
+                        "value": ""
+                      },
+                      {
+                        "name": "keyPoints",
+                        "value": ""
+                      },
+                      {
+                        "name": "keySplines",
+                        "value": ""
+                      },
+                      {
+                        "name": "keyTimes",
+                        "value": ""
+                      },
+                      {
+                        "name": "lengthAdjust",
+                        "value": ""
+                      },
+                      {
+                        "name": "limitingConeAngle",
+                        "value": ""
+                      },
+                      {
+                        "name": "markerHeight",
+                        "value": ""
+                      },
+                      {
+                        "name": "markerUnits",
+                        "value": ""
+                      },
+                      {
+                        "name": "markerWidth",
+                        "value": ""
+                      },
+                      {
+                        "name": "maskContentUnits",
+                        "value": ""
+                      },
+                      {
+                        "name": "maskUnits",
+                        "value": ""
+                      },
+                      {
+                        "name": "numOctaves",
+                        "value": ""
+                      },
+                      {
+                        "name": "pathLength",
+                        "value": ""
+                      },
+                      {
+                        "name": "patternContentUnits",
+                        "value": ""
+                      },
+                      {
+                        "name": "patternTransform",
+                        "value": ""
+                      },
+                      {
+                        "name": "patternUnits",
+                        "value": ""
+                      },
+                      {
+                        "name": "pointsAtX",
+                        "value": ""
+                      },
+                      {
+                        "name": "pointsAtY",
+                        "value": ""
+                      },
+                      {
+                        "name": "pointsAtZ",
+                        "value": ""
+                      },
+                      {
+                        "name": "preserveAlpha",
+                        "value": ""
+                      },
+                      {
+                        "name": "preserveAspectRatio",
+                        "value": ""
+                      },
+                      {
+                        "name": "primitiveUnits",
+                        "value": ""
+                      },
+                      {
+                        "name": "refX",
+                        "value": ""
+                      },
+                      {
+                        "name": "refY",
+                        "value": ""
+                      },
+                      {
+                        "name": "repeatCount",
+                        "value": ""
+                      },
+                      {
+                        "name": "repeatDur",
+                        "value": ""
+                      },
+                      {
+                        "name": "requiredExtensions",
+                        "value": ""
+                      },
+                      {
+                        "name": "requiredFeatures",
+                        "value": ""
+                      },
+                      {
+                        "name": "specularConstant",
+                        "value": ""
+                      },
+                      {
+                        "name": "specularExponent",
+                        "value": ""
+                      },
+                      {
+                        "name": "spreadMethod",
+                        "value": ""
+                      },
+                      {
+                        "name": "startOffset",
+                        "value": ""
+                      },
+                      {
+                        "name": "stdDeviation",
+                        "value": ""
+                      },
+                      {
+                        "name": "stitchTiles",
+                        "value": ""
+                      },
+                      {
+                        "name": "surfaceScale",
+                        "value": ""
+                      },
+                      {
+                        "name": "systemLanguage",
+                        "value": ""
+                      },
+                      {
+                        "name": "tableValues",
+                        "value": ""
+                      },
+                      {
+                        "name": "targetX",
+                        "value": ""
+                      },
+                      {
+                        "name": "targetY",
+                        "value": ""
+                      },
+                      {
+                        "name": "textLength",
+                        "value": ""
+                      },
+                      {
+                        "name": "viewBox",
+                        "value": ""
+                      },
+                      {
+                        "name": "viewTarget",
+                        "value": ""
+                      },
+                      {
+                        "name": "xChannelSelector",
+                        "value": ""
+                      },
+                      {
+                        "name": "yChannelSelector",
+                        "value": ""
+                      },
+                      {
+                        "name": "zoomAndPan",
+                        "value": ""
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><svg attributeName=\"\" attributeType=\"\" baseFrequency=\"\" baseProfile=\"\" calcMode=\"\" clipPathUnits=\"\" diffuseConstant=\"\" edgeMode=\"\" filterUnits=\"\" filterres=\"\" glyphRef=\"\" gradientTransform=\"\" gradientUnits=\"\" kernelMatrix=\"\" kernelUnitLength=\"\" keyPoints=\"\" keySplines=\"\" keyTimes=\"\" lengthAdjust=\"\" limitingConeAngle=\"\" markerHeight=\"\" markerUnits=\"\" markerWidth=\"\" maskContentUnits=\"\" maskUnits=\"\" numOctaves=\"\" pathLength=\"\" patternContentUnits=\"\" patternTransform=\"\" patternUnits=\"\" pointsAtX=\"\" pointsAtY=\"\" pointsAtZ=\"\" preserveAlpha=\"\" preserveAspectRatio=\"\" primitiveUnits=\"\" refX=\"\" refY=\"\" repeatCount=\"\" repeatDur=\"\" requiredExtensions=\"\" requiredFeatures=\"\" specularConstant=\"\" specularExponent=\"\" spreadMethod=\"\" startOffset=\"\" stdDeviation=\"\" stitchTiles=\"\" surfaceScale=\"\" systemLanguage=\"\" tableValues=\"\" targetX=\"\" targetY=\"\" textLength=\"\" viewBox=\"\" viewTarget=\"\" xChannelSelector=\"\" yChannelSelector=\"\" zoomAndPan=\"\"></svg></body></html>",
+        "noQuirksBodyHtml": "<svg attributeName=\"\" attributeType=\"\" baseFrequency=\"\" baseProfile=\"\" calcMode=\"\" clipPathUnits=\"\" diffuseConstant=\"\" edgeMode=\"\" filterUnits=\"\" filterres=\"\" glyphRef=\"\" gradientTransform=\"\" gradientUnits=\"\" kernelMatrix=\"\" kernelUnitLength=\"\" keyPoints=\"\" keySplines=\"\" keyTimes=\"\" lengthAdjust=\"\" limitingConeAngle=\"\" markerHeight=\"\" markerUnits=\"\" markerWidth=\"\" maskContentUnits=\"\" maskUnits=\"\" numOctaves=\"\" pathLength=\"\" patternContentUnits=\"\" patternTransform=\"\" patternUnits=\"\" pointsAtX=\"\" pointsAtY=\"\" pointsAtZ=\"\" preserveAlpha=\"\" preserveAspectRatio=\"\" primitiveUnits=\"\" refX=\"\" refY=\"\" repeatCount=\"\" repeatDur=\"\" requiredExtensions=\"\" requiredFeatures=\"\" specularConstant=\"\" specularExponent=\"\" spreadMethod=\"\" startOffset=\"\" stdDeviation=\"\" stitchTiles=\"\" surfaceScale=\"\" systemLanguage=\"\" tableValues=\"\" targetX=\"\" targetY=\"\" textLength=\"\" viewBox=\"\" viewTarget=\"\" xChannelSelector=\"\" yChannelSelector=\"\" zoomAndPan=\"\"></svg>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><body><math attributeName='' attributeType='' baseFrequency='' baseProfile='' calcMode='' clipPathUnits='' diffuseConstant='' edgeMode='' filterUnits='' glyphRef='' gradientTransform='' gradientUnits='' kernelMatrix='' kernelUnitLength='' keyPoints='' keySplines='' keyTimes='' lengthAdjust='' limitingConeAngle='' markerHeight='' markerUnits='' markerWidth='' maskContentUnits='' maskUnits='' numOctaves='' pathLength='' patternContentUnits='' patternTransform='' patternUnits='' pointsAtX='' pointsAtY='' pointsAtZ='' preserveAlpha='' preserveAspectRatio='' primitiveUnits='' refX='' refY='' repeatCount='' repeatDur='' requiredExtensions='' requiredFeatures='' specularConstant='' specularExponent='' spreadMethod='' startOffset='' stdDeviation='' stitchTiles='' surfaceScale='' systemLanguage='' tableValues='' targetX='' targetY='' textLength='' viewBox='' viewTarget='' xChannelSelector='' yChannelSelector='' zoomAndPan=''></math>",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "math math": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "math",
+                    "ns": "http://www.w3.org/1998/Math/MathML",
+                    "attrs": [
+                      {
+                        "name": "attributename",
+                        "value": ""
+                      },
+                      {
+                        "name": "attributetype",
+                        "value": ""
+                      },
+                      {
+                        "name": "basefrequency",
+                        "value": ""
+                      },
+                      {
+                        "name": "baseprofile",
+                        "value": ""
+                      },
+                      {
+                        "name": "calcmode",
+                        "value": ""
+                      },
+                      {
+                        "name": "clippathunits",
+                        "value": ""
+                      },
+                      {
+                        "name": "diffuseconstant",
+                        "value": ""
+                      },
+                      {
+                        "name": "edgemode",
+                        "value": ""
+                      },
+                      {
+                        "name": "filterunits",
+                        "value": ""
+                      },
+                      {
+                        "name": "glyphref",
+                        "value": ""
+                      },
+                      {
+                        "name": "gradienttransform",
+                        "value": ""
+                      },
+                      {
+                        "name": "gradientunits",
+                        "value": ""
+                      },
+                      {
+                        "name": "kernelmatrix",
+                        "value": ""
+                      },
+                      {
+                        "name": "kernelunitlength",
+                        "value": ""
+                      },
+                      {
+                        "name": "keypoints",
+                        "value": ""
+                      },
+                      {
+                        "name": "keysplines",
+                        "value": ""
+                      },
+                      {
+                        "name": "keytimes",
+                        "value": ""
+                      },
+                      {
+                        "name": "lengthadjust",
+                        "value": ""
+                      },
+                      {
+                        "name": "limitingconeangle",
+                        "value": ""
+                      },
+                      {
+                        "name": "markerheight",
+                        "value": ""
+                      },
+                      {
+                        "name": "markerunits",
+                        "value": ""
+                      },
+                      {
+                        "name": "markerwidth",
+                        "value": ""
+                      },
+                      {
+                        "name": "maskcontentunits",
+                        "value": ""
+                      },
+                      {
+                        "name": "maskunits",
+                        "value": ""
+                      },
+                      {
+                        "name": "numoctaves",
+                        "value": ""
+                      },
+                      {
+                        "name": "pathlength",
+                        "value": ""
+                      },
+                      {
+                        "name": "patterncontentunits",
+                        "value": ""
+                      },
+                      {
+                        "name": "patterntransform",
+                        "value": ""
+                      },
+                      {
+                        "name": "patternunits",
+                        "value": ""
+                      },
+                      {
+                        "name": "pointsatx",
+                        "value": ""
+                      },
+                      {
+                        "name": "pointsaty",
+                        "value": ""
+                      },
+                      {
+                        "name": "pointsatz",
+                        "value": ""
+                      },
+                      {
+                        "name": "preservealpha",
+                        "value": ""
+                      },
+                      {
+                        "name": "preserveaspectratio",
+                        "value": ""
+                      },
+                      {
+                        "name": "primitiveunits",
+                        "value": ""
+                      },
+                      {
+                        "name": "refx",
+                        "value": ""
+                      },
+                      {
+                        "name": "refy",
+                        "value": ""
+                      },
+                      {
+                        "name": "repeatcount",
+                        "value": ""
+                      },
+                      {
+                        "name": "repeatdur",
+                        "value": ""
+                      },
+                      {
+                        "name": "requiredextensions",
+                        "value": ""
+                      },
+                      {
+                        "name": "requiredfeatures",
+                        "value": ""
+                      },
+                      {
+                        "name": "specularconstant",
+                        "value": ""
+                      },
+                      {
+                        "name": "specularexponent",
+                        "value": ""
+                      },
+                      {
+                        "name": "spreadmethod",
+                        "value": ""
+                      },
+                      {
+                        "name": "startoffset",
+                        "value": ""
+                      },
+                      {
+                        "name": "stddeviation",
+                        "value": ""
+                      },
+                      {
+                        "name": "stitchtiles",
+                        "value": ""
+                      },
+                      {
+                        "name": "surfacescale",
+                        "value": ""
+                      },
+                      {
+                        "name": "systemlanguage",
+                        "value": ""
+                      },
+                      {
+                        "name": "tablevalues",
+                        "value": ""
+                      },
+                      {
+                        "name": "targetx",
+                        "value": ""
+                      },
+                      {
+                        "name": "targety",
+                        "value": ""
+                      },
+                      {
+                        "name": "textlength",
+                        "value": ""
+                      },
+                      {
+                        "name": "viewbox",
+                        "value": ""
+                      },
+                      {
+                        "name": "viewtarget",
+                        "value": ""
+                      },
+                      {
+                        "name": "xchannelselector",
+                        "value": ""
+                      },
+                      {
+                        "name": "ychannelselector",
+                        "value": ""
+                      },
+                      {
+                        "name": "zoomandpan",
+                        "value": ""
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><math attributename=\"\" attributetype=\"\" basefrequency=\"\" baseprofile=\"\" calcmode=\"\" clippathunits=\"\" diffuseconstant=\"\" edgemode=\"\" filterunits=\"\" glyphref=\"\" gradienttransform=\"\" gradientunits=\"\" kernelmatrix=\"\" kernelunitlength=\"\" keypoints=\"\" keysplines=\"\" keytimes=\"\" lengthadjust=\"\" limitingconeangle=\"\" markerheight=\"\" markerunits=\"\" markerwidth=\"\" maskcontentunits=\"\" maskunits=\"\" numoctaves=\"\" pathlength=\"\" patterncontentunits=\"\" patterntransform=\"\" patternunits=\"\" pointsatx=\"\" pointsaty=\"\" pointsatz=\"\" preservealpha=\"\" preserveaspectratio=\"\" primitiveunits=\"\" refx=\"\" refy=\"\" repeatcount=\"\" repeatdur=\"\" requiredextensions=\"\" requiredfeatures=\"\" specularconstant=\"\" specularexponent=\"\" spreadmethod=\"\" startoffset=\"\" stddeviation=\"\" stitchtiles=\"\" surfacescale=\"\" systemlanguage=\"\" tablevalues=\"\" targetx=\"\" targety=\"\" textlength=\"\" viewbox=\"\" viewtarget=\"\" xchannelselector=\"\" ychannelselector=\"\" zoomandpan=\"\"></math></body></html>",
+        "noQuirksBodyHtml": "<math attributename=\"\" attributetype=\"\" basefrequency=\"\" baseprofile=\"\" calcmode=\"\" clippathunits=\"\" diffuseconstant=\"\" edgemode=\"\" filterunits=\"\" glyphref=\"\" gradienttransform=\"\" gradientunits=\"\" kernelmatrix=\"\" kernelunitlength=\"\" keypoints=\"\" keysplines=\"\" keytimes=\"\" lengthadjust=\"\" limitingconeangle=\"\" markerheight=\"\" markerunits=\"\" markerwidth=\"\" maskcontentunits=\"\" maskunits=\"\" numoctaves=\"\" pathlength=\"\" patterncontentunits=\"\" patterntransform=\"\" patternunits=\"\" pointsatx=\"\" pointsaty=\"\" pointsatz=\"\" preservealpha=\"\" preserveaspectratio=\"\" primitiveunits=\"\" refx=\"\" refy=\"\" repeatcount=\"\" repeatdur=\"\" requiredextensions=\"\" requiredfeatures=\"\" specularconstant=\"\" specularexponent=\"\" spreadmethod=\"\" startoffset=\"\" stddeviation=\"\" stitchtiles=\"\" surfacescale=\"\" systemlanguage=\"\" tablevalues=\"\" targetx=\"\" targety=\"\" textlength=\"\" viewbox=\"\" viewtarget=\"\" xchannelselector=\"\" ychannelselector=\"\" zoomandpan=\"\"></math>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><body><svg contentScriptType='' contentStyleType='' externalResourcesRequired='' filterRes=''></svg>",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "svg svg": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "svg",
+                    "ns": "http://www.w3.org/2000/svg",
+                    "attrs": [
+                      {
+                        "name": "contentscripttype",
+                        "value": ""
+                      },
+                      {
+                        "name": "contentstyletype",
+                        "value": ""
+                      },
+                      {
+                        "name": "externalresourcesrequired",
+                        "value": ""
+                      },
+                      {
+                        "name": "filterres",
+                        "value": ""
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><svg contentscripttype=\"\" contentstyletype=\"\" externalresourcesrequired=\"\" filterres=\"\"></svg></body></html>",
+        "noQuirksBodyHtml": "<svg contentScriptType=\"\" contentStyleType=\"\" externalResourcesRequired=\"\" filterres=\"\"></svg>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><body><svg CONTENTSCRIPTTYPE='' CONTENTSTYLETYPE='' EXTERNALRESOURCESREQUIRED='' FILTERRES=''></svg>",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "svg svg": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "svg",
+                    "ns": "http://www.w3.org/2000/svg",
+                    "attrs": [
+                      {
+                        "name": "contentscripttype",
+                        "value": ""
+                      },
+                      {
+                        "name": "contentstyletype",
+                        "value": ""
+                      },
+                      {
+                        "name": "externalresourcesrequired",
+                        "value": ""
+                      },
+                      {
+                        "name": "filterres",
+                        "value": ""
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><svg contentscripttype=\"\" contentstyletype=\"\" externalresourcesrequired=\"\" filterres=\"\"></svg></body></html>",
+        "noQuirksBodyHtml": "<svg contentScriptType=\"\" contentStyleType=\"\" externalResourcesRequired=\"\" filterres=\"\"></svg>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><body><svg contentscripttype='' contentstyletype='' externalresourcesrequired='' filterres=''></svg>",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "svg svg": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "svg",
+                    "ns": "http://www.w3.org/2000/svg",
+                    "attrs": [
+                      {
+                        "name": "contentscripttype",
+                        "value": ""
+                      },
+                      {
+                        "name": "contentstyletype",
+                        "value": ""
+                      },
+                      {
+                        "name": "externalresourcesrequired",
+                        "value": ""
+                      },
+                      {
+                        "name": "filterres",
+                        "value": ""
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><svg contentscripttype=\"\" contentstyletype=\"\" externalresourcesrequired=\"\" filterres=\"\"></svg></body></html>",
+        "noQuirksBodyHtml": "<svg contentScriptType=\"\" contentStyleType=\"\" externalResourcesRequired=\"\" filterres=\"\"></svg>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><body><math contentScriptType='' contentStyleType='' externalResourcesRequired='' filterRes=''></math>",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "math math": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "math",
+                    "ns": "http://www.w3.org/1998/Math/MathML",
+                    "attrs": [
+                      {
+                        "name": "contentscripttype",
+                        "value": ""
+                      },
+                      {
+                        "name": "contentstyletype",
+                        "value": ""
+                      },
+                      {
+                        "name": "externalresourcesrequired",
+                        "value": ""
+                      },
+                      {
+                        "name": "filterres",
+                        "value": ""
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><math contentscripttype=\"\" contentstyletype=\"\" externalresourcesrequired=\"\" filterres=\"\"></math></body></html>",
+        "noQuirksBodyHtml": "<math contentscripttype=\"\" contentstyletype=\"\" externalresourcesrequired=\"\" filterres=\"\"></math>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><body><svg><altGlyph /><altGlyphDef /><altGlyphItem /><animateColor /><animateMotion /><animateTransform /><clipPath /><feBlend /><feColorMatrix /><feComponentTransfer /><feComposite /><feConvolveMatrix /><feDiffuseLighting /><feDisplacementMap /><feDistantLight /><feFlood /><feFuncA /><feFuncB /><feFuncG /><feFuncR /><feGaussianBlur /><feImage /><feMerge /><feMergeNode /><feMorphology /><feOffset /><fePointLight /><feSpecularLighting /><feSpotLight /><feTile /><feTurbulence /><foreignObject /><glyphRef /><linearGradient /><radialGradient /><textPath /></svg>",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "svg svg": true,
+            "svg altGlyph": true,
+            "svg altGlyphDef": true,
+            "svg altGlyphItem": true,
+            "svg animateColor": true,
+            "svg animateMotion": true,
+            "svg animateTransform": true,
+            "svg clipPath": true,
+            "svg feBlend": true,
+            "svg feColorMatrix": true,
+            "svg feComponentTransfer": true,
+            "svg feComposite": true,
+            "svg feConvolveMatrix": true,
+            "svg feDiffuseLighting": true,
+            "svg feDisplacementMap": true,
+            "svg feDistantLight": true,
+            "svg feFlood": true,
+            "svg feFuncA": true,
+            "svg feFuncB": true,
+            "svg feFuncG": true,
+            "svg feFuncR": true,
+            "svg feGaussianBlur": true,
+            "svg feImage": true,
+            "svg feMerge": true,
+            "svg feMergeNode": true,
+            "svg feMorphology": true,
+            "svg feOffset": true,
+            "svg fePointLight": true,
+            "svg feSpecularLighting": true,
+            "svg feSpotLight": true,
+            "svg feTile": true,
+            "svg feTurbulence": true,
+            "svg foreignObject": true,
+            "svg glyphRef": true,
+            "svg linearGradient": true,
+            "svg radialGradient": true,
+            "svg textPath": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "svg",
+                    "ns": "http://www.w3.org/2000/svg",
+                    "children": [
+                      {
+                        "tag": "altGlyph",
+                        "ns": "http://www.w3.org/2000/svg"
+                      },
+                      {
+                        "tag": "altGlyphDef",
+                        "ns": "http://www.w3.org/2000/svg"
+                      },
+                      {
+                        "tag": "altGlyphItem",
+                        "ns": "http://www.w3.org/2000/svg"
+                      },
+                      {
+                        "tag": "animateColor",
+                        "ns": "http://www.w3.org/2000/svg"
+                      },
+                      {
+                        "tag": "animateMotion",
+                        "ns": "http://www.w3.org/2000/svg"
+                      },
+                      {
+                        "tag": "animateTransform",
+                        "ns": "http://www.w3.org/2000/svg"
+                      },
+                      {
+                        "tag": "clipPath",
+                        "ns": "http://www.w3.org/2000/svg"
+                      },
+                      {
+                        "tag": "feBlend",
+                        "ns": "http://www.w3.org/2000/svg"
+                      },
+                      {
+                        "tag": "feColorMatrix",
+                        "ns": "http://www.w3.org/2000/svg"
+                      },
+                      {
+                        "tag": "feComponentTransfer",
+                        "ns": "http://www.w3.org/2000/svg"
+                      },
+                      {
+                        "tag": "feComposite",
+                        "ns": "http://www.w3.org/2000/svg"
+                      },
+                      {
+                        "tag": "feConvolveMatrix",
+                        "ns": "http://www.w3.org/2000/svg"
+                      },
+                      {
+                        "tag": "feDiffuseLighting",
+                        "ns": "http://www.w3.org/2000/svg"
+                      },
+                      {
+                        "tag": "feDisplacementMap",
+                        "ns": "http://www.w3.org/2000/svg"
+                      },
+                      {
+                        "tag": "feDistantLight",
+                        "ns": "http://www.w3.org/2000/svg"
+                      },
+                      {
+                        "tag": "feFlood",
+                        "ns": "http://www.w3.org/2000/svg"
+                      },
+                      {
+                        "tag": "feFuncA",
+                        "ns": "http://www.w3.org/2000/svg"
+                      },
+                      {
+                        "tag": "feFuncB",
+                        "ns": "http://www.w3.org/2000/svg"
+                      },
+                      {
+                        "tag": "feFuncG",
+                        "ns": "http://www.w3.org/2000/svg"
+                      },
+                      {
+                        "tag": "feFuncR",
+                        "ns": "http://www.w3.org/2000/svg"
+                      },
+                      {
+                        "tag": "feGaussianBlur",
+                        "ns": "http://www.w3.org/2000/svg"
+                      },
+                      {
+                        "tag": "feImage",
+                        "ns": "http://www.w3.org/2000/svg"
+                      },
+                      {
+                        "tag": "feMerge",
+                        "ns": "http://www.w3.org/2000/svg"
+                      },
+                      {
+                        "tag": "feMergeNode",
+                        "ns": "http://www.w3.org/2000/svg"
+                      },
+                      {
+                        "tag": "feMorphology",
+                        "ns": "http://www.w3.org/2000/svg"
+                      },
+                      {
+                        "tag": "feOffset",
+                        "ns": "http://www.w3.org/2000/svg"
+                      },
+                      {
+                        "tag": "fePointLight",
+                        "ns": "http://www.w3.org/2000/svg"
+                      },
+                      {
+                        "tag": "feSpecularLighting",
+                        "ns": "http://www.w3.org/2000/svg"
+                      },
+                      {
+                        "tag": "feSpotLight",
+                        "ns": "http://www.w3.org/2000/svg"
+                      },
+                      {
+                        "tag": "feTile",
+                        "ns": "http://www.w3.org/2000/svg"
+                      },
+                      {
+                        "tag": "feTurbulence",
+                        "ns": "http://www.w3.org/2000/svg"
+                      },
+                      {
+                        "tag": "foreignObject",
+                        "ns": "http://www.w3.org/2000/svg"
+                      },
+                      {
+                        "tag": "glyphRef",
+                        "ns": "http://www.w3.org/2000/svg"
+                      },
+                      {
+                        "tag": "linearGradient",
+                        "ns": "http://www.w3.org/2000/svg"
+                      },
+                      {
+                        "tag": "radialGradient",
+                        "ns": "http://www.w3.org/2000/svg"
+                      },
+                      {
+                        "tag": "textPath",
+                        "ns": "http://www.w3.org/2000/svg"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><svg><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></svg></body></html>",
+        "noQuirksBodyHtml": "<svg><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></svg>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><body><svg><altglyph /><altglyphdef /><altglyphitem /><animatecolor /><animatemotion /><animatetransform /><clippath /><feblend /><fecolormatrix /><fecomponenttransfer /><fecomposite /><feconvolvematrix /><fediffuselighting /><fedisplacementmap /><fedistantlight /><feflood /><fefunca /><fefuncb /><fefuncg /><fefuncr /><fegaussianblur /><feimage /><femerge /><femergenode /><femorphology /><feoffset /><fepointlight /><fespecularlighting /><fespotlight /><fetile /><feturbulence /><foreignobject /><glyphref /><lineargradient /><radialgradient /><textpath /></svg>",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "svg svg": true,
+            "svg altGlyph": true,
+            "svg altGlyphDef": true,
+            "svg altGlyphItem": true,
+            "svg animateColor": true,
+            "svg animateMotion": true,
+            "svg animateTransform": true,
+            "svg clipPath": true,
+            "svg feBlend": true,
+            "svg feColorMatrix": true,
+            "svg feComponentTransfer": true,
+            "svg feComposite": true,
+            "svg feConvolveMatrix": true,
+            "svg feDiffuseLighting": true,
+            "svg feDisplacementMap": true,
+            "svg feDistantLight": true,
+            "svg feFlood": true,
+            "svg feFuncA": true,
+            "svg feFuncB": true,
+            "svg feFuncG": true,
+            "svg feFuncR": true,
+            "svg feGaussianBlur": true,
+            "svg feImage": true,
+            "svg feMerge": true,
+            "svg feMergeNode": true,
+            "svg feMorphology": true,
+            "svg feOffset": true,
+            "svg fePointLight": true,
+            "svg feSpecularLighting": true,
+            "svg feSpotLight": true,
+            "svg feTile": true,
+            "svg feTurbulence": true,
+            "svg foreignObject": true,
+            "svg glyphRef": true,
+            "svg linearGradient": true,
+            "svg radialGradient": true,
+            "svg textPath": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "svg",
+                    "ns": "http://www.w3.org/2000/svg",
+                    "children": [
+                      {
+                        "tag": "altGlyph",
+                        "ns": "http://www.w3.org/2000/svg"
+                      },
+                      {
+                        "tag": "altGlyphDef",
+                        "ns": "http://www.w3.org/2000/svg"
+                      },
+                      {
+                        "tag": "altGlyphItem",
+                        "ns": "http://www.w3.org/2000/svg"
+                      },
+                      {
+                        "tag": "animateColor",
+                        "ns": "http://www.w3.org/2000/svg"
+                      },
+                      {
+                        "tag": "animateMotion",
+                        "ns": "http://www.w3.org/2000/svg"
+                      },
+                      {
+                        "tag": "animateTransform",
+                        "ns": "http://www.w3.org/2000/svg"
+                      },
+                      {
+                        "tag": "clipPath",
+                        "ns": "http://www.w3.org/2000/svg"
+                      },
+                      {
+                        "tag": "feBlend",
+                        "ns": "http://www.w3.org/2000/svg"
+                      },
+                      {
+                        "tag": "feColorMatrix",
+                        "ns": "http://www.w3.org/2000/svg"
+                      },
+                      {
+                        "tag": "feComponentTransfer",
+                        "ns": "http://www.w3.org/2000/svg"
+                      },
+                      {
+                        "tag": "feComposite",
+                        "ns": "http://www.w3.org/2000/svg"
+                      },
+                      {
+                        "tag": "feConvolveMatrix",
+                        "ns": "http://www.w3.org/2000/svg"
+                      },
+                      {
+                        "tag": "feDiffuseLighting",
+                        "ns": "http://www.w3.org/2000/svg"
+                      },
+                      {
+                        "tag": "feDisplacementMap",
+                        "ns": "http://www.w3.org/2000/svg"
+                      },
+                      {
+                        "tag": "feDistantLight",
+                        "ns": "http://www.w3.org/2000/svg"
+                      },
+                      {
+                        "tag": "feFlood",
+                        "ns": "http://www.w3.org/2000/svg"
+                      },
+                      {
+                        "tag": "feFuncA",
+                        "ns": "http://www.w3.org/2000/svg"
+                      },
+                      {
+                        "tag": "feFuncB",
+                        "ns": "http://www.w3.org/2000/svg"
+                      },
+                      {
+                        "tag": "feFuncG",
+                        "ns": "http://www.w3.org/2000/svg"
+                      },
+                      {
+                        "tag": "feFuncR",
+                        "ns": "http://www.w3.org/2000/svg"
+                      },
+                      {
+                        "tag": "feGaussianBlur",
+                        "ns": "http://www.w3.org/2000/svg"
+                      },
+                      {
+                        "tag": "feImage",
+                        "ns": "http://www.w3.org/2000/svg"
+                      },
+                      {
+                        "tag": "feMerge",
+                        "ns": "http://www.w3.org/2000/svg"
+                      },
+                      {
+                        "tag": "feMergeNode",
+                        "ns": "http://www.w3.org/2000/svg"
+                      },
+                      {
+                        "tag": "feMorphology",
+                        "ns": "http://www.w3.org/2000/svg"
+                      },
+                      {
+                        "tag": "feOffset",
+                        "ns": "http://www.w3.org/2000/svg"
+                      },
+                      {
+                        "tag": "fePointLight",
+                        "ns": "http://www.w3.org/2000/svg"
+                      },
+                      {
+                        "tag": "feSpecularLighting",
+                        "ns": "http://www.w3.org/2000/svg"
+                      },
+                      {
+                        "tag": "feSpotLight",
+                        "ns": "http://www.w3.org/2000/svg"
+                      },
+                      {
+                        "tag": "feTile",
+                        "ns": "http://www.w3.org/2000/svg"
+                      },
+                      {
+                        "tag": "feTurbulence",
+                        "ns": "http://www.w3.org/2000/svg"
+                      },
+                      {
+                        "tag": "foreignObject",
+                        "ns": "http://www.w3.org/2000/svg"
+                      },
+                      {
+                        "tag": "glyphRef",
+                        "ns": "http://www.w3.org/2000/svg"
+                      },
+                      {
+                        "tag": "linearGradient",
+                        "ns": "http://www.w3.org/2000/svg"
+                      },
+                      {
+                        "tag": "radialGradient",
+                        "ns": "http://www.w3.org/2000/svg"
+                      },
+                      {
+                        "tag": "textPath",
+                        "ns": "http://www.w3.org/2000/svg"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><svg><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></svg></body></html>",
+        "noQuirksBodyHtml": "<svg><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></svg>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><BODY><SVG><ALTGLYPH /><ALTGLYPHDEF /><ALTGLYPHITEM /><ANIMATECOLOR /><ANIMATEMOTION /><ANIMATETRANSFORM /><CLIPPATH /><FEBLEND /><FECOLORMATRIX /><FECOMPONENTTRANSFER /><FECOMPOSITE /><FECONVOLVEMATRIX /><FEDIFFUSELIGHTING /><FEDISPLACEMENTMAP /><FEDISTANTLIGHT /><FEFLOOD /><FEFUNCA /><FEFUNCB /><FEFUNCG /><FEFUNCR /><FEGAUSSIANBLUR /><FEIMAGE /><FEMERGE /><FEMERGENODE /><FEMORPHOLOGY /><FEOFFSET /><FEPOINTLIGHT /><FESPECULARLIGHTING /><FESPOTLIGHT /><FETILE /><FETURBULENCE /><FOREIGNOBJECT /><GLYPHREF /><LINEARGRADIENT /><RADIALGRADIENT /><TEXTPATH /></SVG>",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "svg svg": true,
+            "svg altGlyph": true,
+            "svg altGlyphDef": true,
+            "svg altGlyphItem": true,
+            "svg animateColor": true,
+            "svg animateMotion": true,
+            "svg animateTransform": true,
+            "svg clipPath": true,
+            "svg feBlend": true,
+            "svg feColorMatrix": true,
+            "svg feComponentTransfer": true,
+            "svg feComposite": true,
+            "svg feConvolveMatrix": true,
+            "svg feDiffuseLighting": true,
+            "svg feDisplacementMap": true,
+            "svg feDistantLight": true,
+            "svg feFlood": true,
+            "svg feFuncA": true,
+            "svg feFuncB": true,
+            "svg feFuncG": true,
+            "svg feFuncR": true,
+            "svg feGaussianBlur": true,
+            "svg feImage": true,
+            "svg feMerge": true,
+            "svg feMergeNode": true,
+            "svg feMorphology": true,
+            "svg feOffset": true,
+            "svg fePointLight": true,
+            "svg feSpecularLighting": true,
+            "svg feSpotLight": true,
+            "svg feTile": true,
+            "svg feTurbulence": true,
+            "svg foreignObject": true,
+            "svg glyphRef": true,
+            "svg linearGradient": true,
+            "svg radialGradient": true,
+            "svg textPath": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "svg",
+                    "ns": "http://www.w3.org/2000/svg",
+                    "children": [
+                      {
+                        "tag": "altGlyph",
+                        "ns": "http://www.w3.org/2000/svg"
+                      },
+                      {
+                        "tag": "altGlyphDef",
+                        "ns": "http://www.w3.org/2000/svg"
+                      },
+                      {
+                        "tag": "altGlyphItem",
+                        "ns": "http://www.w3.org/2000/svg"
+                      },
+                      {
+                        "tag": "animateColor",
+                        "ns": "http://www.w3.org/2000/svg"
+                      },
+                      {
+                        "tag": "animateMotion",
+                        "ns": "http://www.w3.org/2000/svg"
+                      },
+                      {
+                        "tag": "animateTransform",
+                        "ns": "http://www.w3.org/2000/svg"
+                      },
+                      {
+                        "tag": "clipPath",
+                        "ns": "http://www.w3.org/2000/svg"
+                      },
+                      {
+                        "tag": "feBlend",
+                        "ns": "http://www.w3.org/2000/svg"
+                      },
+                      {
+                        "tag": "feColorMatrix",
+                        "ns": "http://www.w3.org/2000/svg"
+                      },
+                      {
+                        "tag": "feComponentTransfer",
+                        "ns": "http://www.w3.org/2000/svg"
+                      },
+                      {
+                        "tag": "feComposite",
+                        "ns": "http://www.w3.org/2000/svg"
+                      },
+                      {
+                        "tag": "feConvolveMatrix",
+                        "ns": "http://www.w3.org/2000/svg"
+                      },
+                      {
+                        "tag": "feDiffuseLighting",
+                        "ns": "http://www.w3.org/2000/svg"
+                      },
+                      {
+                        "tag": "feDisplacementMap",
+                        "ns": "http://www.w3.org/2000/svg"
+                      },
+                      {
+                        "tag": "feDistantLight",
+                        "ns": "http://www.w3.org/2000/svg"
+                      },
+                      {
+                        "tag": "feFlood",
+                        "ns": "http://www.w3.org/2000/svg"
+                      },
+                      {
+                        "tag": "feFuncA",
+                        "ns": "http://www.w3.org/2000/svg"
+                      },
+                      {
+                        "tag": "feFuncB",
+                        "ns": "http://www.w3.org/2000/svg"
+                      },
+                      {
+                        "tag": "feFuncG",
+                        "ns": "http://www.w3.org/2000/svg"
+                      },
+                      {
+                        "tag": "feFuncR",
+                        "ns": "http://www.w3.org/2000/svg"
+                      },
+                      {
+                        "tag": "feGaussianBlur",
+                        "ns": "http://www.w3.org/2000/svg"
+                      },
+                      {
+                        "tag": "feImage",
+                        "ns": "http://www.w3.org/2000/svg"
+                      },
+                      {
+                        "tag": "feMerge",
+                        "ns": "http://www.w3.org/2000/svg"
+                      },
+                      {
+                        "tag": "feMergeNode",
+                        "ns": "http://www.w3.org/2000/svg"
+                      },
+                      {
+                        "tag": "feMorphology",
+                        "ns": "http://www.w3.org/2000/svg"
+                      },
+                      {
+                        "tag": "feOffset",
+                        "ns": "http://www.w3.org/2000/svg"
+                      },
+                      {
+                        "tag": "fePointLight",
+                        "ns": "http://www.w3.org/2000/svg"
+                      },
+                      {
+                        "tag": "feSpecularLighting",
+                        "ns": "http://www.w3.org/2000/svg"
+                      },
+                      {
+                        "tag": "feSpotLight",
+                        "ns": "http://www.w3.org/2000/svg"
+                      },
+                      {
+                        "tag": "feTile",
+                        "ns": "http://www.w3.org/2000/svg"
+                      },
+                      {
+                        "tag": "feTurbulence",
+                        "ns": "http://www.w3.org/2000/svg"
+                      },
+                      {
+                        "tag": "foreignObject",
+                        "ns": "http://www.w3.org/2000/svg"
+                      },
+                      {
+                        "tag": "glyphRef",
+                        "ns": "http://www.w3.org/2000/svg"
+                      },
+                      {
+                        "tag": "linearGradient",
+                        "ns": "http://www.w3.org/2000/svg"
+                      },
+                      {
+                        "tag": "radialGradient",
+                        "ns": "http://www.w3.org/2000/svg"
+                      },
+                      {
+                        "tag": "textPath",
+                        "ns": "http://www.w3.org/2000/svg"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><svg><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></svg></body></html>",
+        "noQuirksBodyHtml": "<svg><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></svg>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><body><math><altGlyph /><altGlyphDef /><altGlyphItem /><animateColor /><animateMotion /><animateTransform /><clipPath /><feBlend /><feColorMatrix /><feComponentTransfer /><feComposite /><feConvolveMatrix /><feDiffuseLighting /><feDisplacementMap /><feDistantLight /><feFlood /><feFuncA /><feFuncB /><feFuncG /><feFuncR /><feGaussianBlur /><feImage /><feMerge /><feMergeNode /><feMorphology /><feOffset /><fePointLight /><feSpecularLighting /><feSpotLight /><feTile /><feTurbulence /><foreignObject /><glyphRef /><linearGradient /><radialGradient /><textPath /></math>",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "math math": true,
+            "math altglyph": true,
+            "math altglyphdef": true,
+            "math altglyphitem": true,
+            "math animatecolor": true,
+            "math animatemotion": true,
+            "math animatetransform": true,
+            "math clippath": true,
+            "math feblend": true,
+            "math fecolormatrix": true,
+            "math fecomponenttransfer": true,
+            "math fecomposite": true,
+            "math feconvolvematrix": true,
+            "math fediffuselighting": true,
+            "math fedisplacementmap": true,
+            "math fedistantlight": true,
+            "math feflood": true,
+            "math fefunca": true,
+            "math fefuncb": true,
+            "math fefuncg": true,
+            "math fefuncr": true,
+            "math fegaussianblur": true,
+            "math feimage": true,
+            "math femerge": true,
+            "math femergenode": true,
+            "math femorphology": true,
+            "math feoffset": true,
+            "math fepointlight": true,
+            "math fespecularlighting": true,
+            "math fespotlight": true,
+            "math fetile": true,
+            "math feturbulence": true,
+            "math foreignobject": true,
+            "math glyphref": true,
+            "math lineargradient": true,
+            "math radialgradient": true,
+            "math textpath": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "math",
+                    "ns": "http://www.w3.org/1998/Math/MathML",
+                    "children": [
+                      {
+                        "tag": "altglyph",
+                        "ns": "http://www.w3.org/1998/Math/MathML"
+                      },
+                      {
+                        "tag": "altglyphdef",
+                        "ns": "http://www.w3.org/1998/Math/MathML"
+                      },
+                      {
+                        "tag": "altglyphitem",
+                        "ns": "http://www.w3.org/1998/Math/MathML"
+                      },
+                      {
+                        "tag": "animatecolor",
+                        "ns": "http://www.w3.org/1998/Math/MathML"
+                      },
+                      {
+                        "tag": "animatemotion",
+                        "ns": "http://www.w3.org/1998/Math/MathML"
+                      },
+                      {
+                        "tag": "animatetransform",
+                        "ns": "http://www.w3.org/1998/Math/MathML"
+                      },
+                      {
+                        "tag": "clippath",
+                        "ns": "http://www.w3.org/1998/Math/MathML"
+                      },
+                      {
+                        "tag": "feblend",
+                        "ns": "http://www.w3.org/1998/Math/MathML"
+                      },
+                      {
+                        "tag": "fecolormatrix",
+                        "ns": "http://www.w3.org/1998/Math/MathML"
+                      },
+                      {
+                        "tag": "fecomponenttransfer",
+                        "ns": "http://www.w3.org/1998/Math/MathML"
+                      },
+                      {
+                        "tag": "fecomposite",
+                        "ns": "http://www.w3.org/1998/Math/MathML"
+                      },
+                      {
+                        "tag": "feconvolvematrix",
+                        "ns": "http://www.w3.org/1998/Math/MathML"
+                      },
+                      {
+                        "tag": "fediffuselighting",
+                        "ns": "http://www.w3.org/1998/Math/MathML"
+                      },
+                      {
+                        "tag": "fedisplacementmap",
+                        "ns": "http://www.w3.org/1998/Math/MathML"
+                      },
+                      {
+                        "tag": "fedistantlight",
+                        "ns": "http://www.w3.org/1998/Math/MathML"
+                      },
+                      {
+                        "tag": "feflood",
+                        "ns": "http://www.w3.org/1998/Math/MathML"
+                      },
+                      {
+                        "tag": "fefunca",
+                        "ns": "http://www.w3.org/1998/Math/MathML"
+                      },
+                      {
+                        "tag": "fefuncb",
+                        "ns": "http://www.w3.org/1998/Math/MathML"
+                      },
+                      {
+                        "tag": "fefuncg",
+                        "ns": "http://www.w3.org/1998/Math/MathML"
+                      },
+                      {
+                        "tag": "fefuncr",
+                        "ns": "http://www.w3.org/1998/Math/MathML"
+                      },
+                      {
+                        "tag": "fegaussianblur",
+                        "ns": "http://www.w3.org/1998/Math/MathML"
+                      },
+                      {
+                        "tag": "feimage",
+                        "ns": "http://www.w3.org/1998/Math/MathML"
+                      },
+                      {
+                        "tag": "femerge",
+                        "ns": "http://www.w3.org/1998/Math/MathML"
+                      },
+                      {
+                        "tag": "femergenode",
+                        "ns": "http://www.w3.org/1998/Math/MathML"
+                      },
+                      {
+                        "tag": "femorphology",
+                        "ns": "http://www.w3.org/1998/Math/MathML"
+                      },
+                      {
+                        "tag": "feoffset",
+                        "ns": "http://www.w3.org/1998/Math/MathML"
+                      },
+                      {
+                        "tag": "fepointlight",
+                        "ns": "http://www.w3.org/1998/Math/MathML"
+                      },
+                      {
+                        "tag": "fespecularlighting",
+                        "ns": "http://www.w3.org/1998/Math/MathML"
+                      },
+                      {
+                        "tag": "fespotlight",
+                        "ns": "http://www.w3.org/1998/Math/MathML"
+                      },
+                      {
+                        "tag": "fetile",
+                        "ns": "http://www.w3.org/1998/Math/MathML"
+                      },
+                      {
+                        "tag": "feturbulence",
+                        "ns": "http://www.w3.org/1998/Math/MathML"
+                      },
+                      {
+                        "tag": "foreignobject",
+                        "ns": "http://www.w3.org/1998/Math/MathML"
+                      },
+                      {
+                        "tag": "glyphref",
+                        "ns": "http://www.w3.org/1998/Math/MathML"
+                      },
+                      {
+                        "tag": "lineargradient",
+                        "ns": "http://www.w3.org/1998/Math/MathML"
+                      },
+                      {
+                        "tag": "radialgradient",
+                        "ns": "http://www.w3.org/1998/Math/MathML"
+                      },
+                      {
+                        "tag": "textpath",
+                        "ns": "http://www.w3.org/1998/Math/MathML"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><math><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></math></body></html>",
+        "noQuirksBodyHtml": "<math><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></math>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><body><svg><solidColor /></svg>",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "svg svg": true,
+            "svg solidcolor": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "svg",
+                    "ns": "http://www.w3.org/2000/svg",
+                    "children": [
+                      {
+                        "tag": "solidcolor",
+                        "ns": "http://www.w3.org/2000/svg"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><svg><solidcolor></solidcolor></svg></body></html>",
+        "noQuirksBodyHtml": "<svg><solidcolor></solidcolor></svg>"
+      }
+    }
+  ],
+  "tests12.dat": [
+    {
+      "data": "<!DOCTYPE html><body><p>foo<math><mtext><i>baz</i></mtext><annotation-xml><svg><desc><b>eggs</b></desc><g><foreignObject><P>spam<TABLE><tr><td><img></td></table></foreignObject></g><g>quux</g></svg></annotation-xml></math>bar",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "p": true,
+            "math math": true,
+            "math mtext": true,
+            "i": true,
+            "math annotation-xml": true,
+            "svg svg": true,
+            "svg desc": true,
+            "b": true,
+            "svg g": true,
+            "svg foreignObject": true,
+            "table": true,
+            "tbody": true,
+            "tr": true,
+            "td": true,
+            "img": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "p",
+                    "children": [
+                      {
+                        "text": "foo"
+                      },
+                      {
+                        "tag": "math",
+                        "ns": "http://www.w3.org/1998/Math/MathML",
+                        "children": [
+                          {
+                            "tag": "mtext",
+                            "ns": "http://www.w3.org/1998/Math/MathML",
+                            "children": [
+                              {
+                                "tag": "i",
+                                "children": [
+                                  {
+                                    "text": "baz"
+                                  }
+                                ]
+                              }
+                            ]
+                          },
+                          {
+                            "tag": "annotation-xml",
+                            "ns": "http://www.w3.org/1998/Math/MathML",
+                            "children": [
+                              {
+                                "tag": "svg",
+                                "ns": "http://www.w3.org/2000/svg",
+                                "children": [
+                                  {
+                                    "tag": "desc",
+                                    "ns": "http://www.w3.org/2000/svg",
+                                    "children": [
+                                      {
+                                        "tag": "b",
+                                        "children": [
+                                          {
+                                            "text": "eggs"
+                                          }
+                                        ]
+                                      }
+                                    ]
+                                  },
+                                  {
+                                    "tag": "g",
+                                    "ns": "http://www.w3.org/2000/svg",
+                                    "children": [
+                                      {
+                                        "tag": "foreignObject",
+                                        "ns": "http://www.w3.org/2000/svg",
+                                        "children": [
+                                          {
+                                            "tag": "p",
+                                            "children": [
+                                              {
+                                                "text": "spam"
+                                              }
+                                            ]
+                                          },
+                                          {
+                                            "tag": "table",
+                                            "children": [
+                                              {
+                                                "tag": "tbody",
+                                                "children": [
+                                                  {
+                                                    "tag": "tr",
+                                                    "children": [
+                                                      {
+                                                        "tag": "td",
+                                                        "children": [
+                                                          {
+                                                            "tag": "img"
+                                                          }
+                                                        ]
+                                                      }
+                                                    ]
+                                                  }
+                                                ]
+                                              }
+                                            ]
+                                          }
+                                        ]
+                                      }
+                                    ]
+                                  },
+                                  {
+                                    "tag": "g",
+                                    "ns": "http://www.w3.org/2000/svg",
+                                    "children": [
+                                      {
+                                        "text": "quux"
+                                      }
+                                    ]
+                                  }
+                                ]
+                              }
+                            ]
+                          }
+                        ]
+                      },
+                      {
+                        "text": "bar"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><p>foo<math><mtext><i>baz</i></mtext><annotation-xml><svg><desc><b>eggs</b></desc><g><foreignObject><p>spam</p><table><tbody><tr><td><img></td></tr></tbody></table></foreignObject></g><g>quux</g></svg></annotation-xml></math>bar</p></body></html>",
+        "noQuirksBodyHtml": "<p>foo<math><mtext><i>baz</i></mtext><annotation-xml><svg><desc><b>eggs</b></desc><g><foreignObject><p>spam</p><table><tbody><tr><td><img></td></tr></tbody></table></foreignObject></g><g>quux</g></svg></annotation-xml></math>bar</p>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><body>foo<math><mtext><i>baz</i></mtext><annotation-xml><svg><desc><b>eggs</b></desc><g><foreignObject><P>spam<TABLE><tr><td><img></td></table></foreignObject></g><g>quux</g></svg></annotation-xml></math>bar",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "math math": true,
+            "math mtext": true,
+            "i": true,
+            "math annotation-xml": true,
+            "svg svg": true,
+            "svg desc": true,
+            "b": true,
+            "svg g": true,
+            "svg foreignObject": true,
+            "p": true,
+            "table": true,
+            "tbody": true,
+            "tr": true,
+            "td": true,
+            "img": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "foo"
+                  },
+                  {
+                    "tag": "math",
+                    "ns": "http://www.w3.org/1998/Math/MathML",
+                    "children": [
+                      {
+                        "tag": "mtext",
+                        "ns": "http://www.w3.org/1998/Math/MathML",
+                        "children": [
+                          {
+                            "tag": "i",
+                            "children": [
+                              {
+                                "text": "baz"
+                              }
+                            ]
+                          }
+                        ]
+                      },
+                      {
+                        "tag": "annotation-xml",
+                        "ns": "http://www.w3.org/1998/Math/MathML",
+                        "children": [
+                          {
+                            "tag": "svg",
+                            "ns": "http://www.w3.org/2000/svg",
+                            "children": [
+                              {
+                                "tag": "desc",
+                                "ns": "http://www.w3.org/2000/svg",
+                                "children": [
+                                  {
+                                    "tag": "b",
+                                    "children": [
+                                      {
+                                        "text": "eggs"
+                                      }
+                                    ]
+                                  }
+                                ]
+                              },
+                              {
+                                "tag": "g",
+                                "ns": "http://www.w3.org/2000/svg",
+                                "children": [
+                                  {
+                                    "tag": "foreignObject",
+                                    "ns": "http://www.w3.org/2000/svg",
+                                    "children": [
+                                      {
+                                        "tag": "p",
+                                        "children": [
+                                          {
+                                            "text": "spam"
+                                          }
+                                        ]
+                                      },
+                                      {
+                                        "tag": "table",
+                                        "children": [
+                                          {
+                                            "tag": "tbody",
+                                            "children": [
+                                              {
+                                                "tag": "tr",
+                                                "children": [
+                                                  {
+                                                    "tag": "td",
+                                                    "children": [
+                                                      {
+                                                        "tag": "img"
+                                                      }
+                                                    ]
+                                                  }
+                                                ]
+                                              }
+                                            ]
+                                          }
+                                        ]
+                                      }
+                                    ]
+                                  }
+                                ]
+                              },
+                              {
+                                "tag": "g",
+                                "ns": "http://www.w3.org/2000/svg",
+                                "children": [
+                                  {
+                                    "text": "quux"
+                                  }
+                                ]
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  },
+                  {
+                    "text": "bar"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body>foo<math><mtext><i>baz</i></mtext><annotation-xml><svg><desc><b>eggs</b></desc><g><foreignObject><p>spam</p><table><tbody><tr><td><img></td></tr></tbody></table></foreignObject></g><g>quux</g></svg></annotation-xml></math>bar</body></html>",
+        "noQuirksBodyHtml": "foo<math><mtext><i>baz</i></mtext><annotation-xml><svg><desc><b>eggs</b></desc><g><foreignObject><p>spam</p><table><tbody><tr><td><img></td></tr></tbody></table></foreignObject></g><g>quux</g></svg></annotation-xml></math>bar"
+      }
+    }
+  ],
+  "tests14.dat": [
+    {
+      "data": "<!DOCTYPE html><html><body><xyz:abc></xyz:abc>",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "xyz:abc": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "xyz:abc"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><xyz:abc></xyz:abc></body></html>",
+        "noQuirksBodyHtml": "<xyz:abc></xyz:abc>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><html><body><xyz:abc></xyz:abc><span></span>",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "xyz:abc": true,
+            "span": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "xyz:abc"
+                  },
+                  {
+                    "tag": "span"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><xyz:abc></xyz:abc><span></span></body></html>",
+        "noQuirksBodyHtml": "<xyz:abc></xyz:abc><span></span>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><html><html abc:def=gh><xyz:abc></xyz:abc>",
+      "errors": [
+        "(1,38): non-html-root"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "xyz:abc": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "attrs": [
+              {
+                "name": "abc:def",
+                "value": "gh"
+              }
+            ],
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "xyz:abc"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html abc:def=\"gh\"><head></head><body><xyz:abc></xyz:abc></body></html>",
+        "noQuirksBodyHtml": "<xyz:abc></xyz:abc>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><html xml:lang=bar><html xml:lang=foo>",
+      "errors": [
+        "(1,53): non-html-root"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "attrs": [
+              {
+                "name": "xml:lang",
+                "value": "bar"
+              }
+            ],
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html xml:lang=\"bar\"><head></head><body></body></html>",
+        "noQuirksBodyHtml": ""
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><html 123=456>",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "attrs": [
+              {
+                "name": "123",
+                "value": "456"
+              }
+            ],
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html 123=\"456\"><head></head><body></body></html>",
+        "noQuirksBodyHtml": ""
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><html 123=456><html 789=012>",
+      "errors": [
+        "(1,43): non-html-root"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "attrs": [
+              {
+                "name": "123",
+                "value": "456"
+              },
+              {
+                "name": "789",
+                "value": "012"
+              }
+            ],
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html 123=\"456\" 789=\"012\"><head></head><body></body></html>",
+        "noQuirksBodyHtml": ""
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><html><body 789=012>",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "attrs": [
+                  {
+                    "name": "789",
+                    "value": "012"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body 789=\"012\"></body></html>",
+        "noQuirksBodyHtml": ""
+      }
+    }
+  ],
+  "tests15.dat": [
+    {
+      "data": "<!DOCTYPE html><p><b><i><u></p> <p>X",
+      "errors": [
+        "(1,31): unexpected-end-tag",
+        "(1,36): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "p": true,
+            "b": true,
+            "i": true,
+            "u": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "p",
+                    "children": [
+                      {
+                        "tag": "b",
+                        "children": [
+                          {
+                            "tag": "i",
+                            "children": [
+                              {
+                                "tag": "u"
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "b",
+                    "children": [
+                      {
+                        "tag": "i",
+                        "children": [
+                          {
+                            "tag": "u",
+                            "children": [
+                              {
+                                "text": " "
+                              },
+                              {
+                                "tag": "p",
+                                "children": [
+                                  {
+                                    "text": "X"
+                                  }
+                                ]
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><p><b><i><u></u></i></b></p><b><i><u> <p>X</p></u></i></b></body></html>",
+        "noQuirksBodyHtml": "<p><b><i><u></u></i></b></p><b><i><u> <p>X</p></u></i></b>"
+      }
+    },
+    {
+      "data": "<p><b><i><u></p>\n<p>X",
+      "errors": [
+        "(1,3): expected-doctype-but-got-start-tag",
+        "(1,16): unexpected-end-tag",
+        "(2,4): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "p": true,
+            "b": true,
+            "i": true,
+            "u": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "p",
+                    "children": [
+                      {
+                        "tag": "b",
+                        "children": [
+                          {
+                            "tag": "i",
+                            "children": [
+                              {
+                                "tag": "u"
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "b",
+                    "children": [
+                      {
+                        "tag": "i",
+                        "children": [
+                          {
+                            "tag": "u",
+                            "children": [
+                              {
+                                "text": "\n"
+                              },
+                              {
+                                "tag": "p",
+                                "children": [
+                                  {
+                                    "text": "X"
+                                  }
+                                ]
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><p><b><i><u></u></i></b></p><b><i><u>\n<p>X</p></u></i></b></body></html>",
+        "noQuirksBodyHtml": "<p><b><i><u></u></i></b></p><b><i><u>\n<p>X</p></u></i></b>"
+      }
+    },
+    {
+      "data": "<!doctype html></html> <head>",
+      "errors": [
+        "(1,29): expected-eof-but-got-start-tag",
+        "(1,29): unexpected-start-tag-ignored"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": " "
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body> </body></html>",
+        "noQuirksBodyHtml": " "
+      }
+    },
+    {
+      "data": "<!doctype html></body><meta>",
+      "errors": [
+        "(1,28): unexpected-start-tag-after-body"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "meta": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "meta"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><meta></body></html>",
+        "noQuirksBodyHtml": "<meta>"
+      }
+    },
+    {
+      "data": "<html></html><!-- foo -->",
+      "errors": [
+        "(1,6): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "comment": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          },
+          {
+            "comment": " foo "
+          }
+        ],
+        "html": "<html><head></head><body></body></html><!-- foo -->",
+        "noQuirksBodyHtml": "<!-- foo -->"
+      }
+    },
+    {
+      "data": "<!doctype html></body><title>X</title>",
+      "errors": [
+        "(1,29): unexpected-start-tag-after-body"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "title": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "title",
+                    "children": [
+                      {
+                        "text": "X"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><title>X</title></body></html>",
+        "noQuirksBodyHtml": "<title>X</title>"
+      }
+    },
+    {
+      "data": "<!doctype html><table> X<meta></table>",
+      "errors": [
+        "(1,23): foster-parenting-character",
+        "(1,24): foster-parenting-character",
+        "(1,30): foster-parenting-start-character"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "meta": true,
+            "table": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": " X"
+                  },
+                  {
+                    "tag": "meta"
+                  },
+                  {
+                    "tag": "table"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body> X<meta><table></table></body></html>",
+        "noQuirksBodyHtml": " X<meta><table></table>"
+      }
+    },
+    {
+      "data": "<!doctype html><table> x</table>",
+      "errors": [
+        "(1,23): foster-parenting-character",
+        "(1,24): foster-parenting-character"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": " x"
+                  },
+                  {
+                    "tag": "table"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body> x<table></table></body></html>",
+        "noQuirksBodyHtml": " x<table></table>"
+      }
+    },
+    {
+      "data": "<!doctype html><table> x </table>",
+      "errors": [
+        "(1,23): foster-parenting-character",
+        "(1,24): foster-parenting-character",
+        "(1,25): foster-parenting-character"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": " x "
+                  },
+                  {
+                    "tag": "table"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body> x <table></table></body></html>",
+        "noQuirksBodyHtml": " x <table></table>"
+      }
+    },
+    {
+      "data": "<!doctype html><table><tr> x</table>",
+      "errors": [
+        "(1,27): foster-parenting-character",
+        "(1,28): foster-parenting-character"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true,
+            "tbody": true,
+            "tr": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": " x"
+                  },
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "tbody",
+                        "children": [
+                          {
+                            "tag": "tr"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body> x<table><tbody><tr></tr></tbody></table></body></html>",
+        "noQuirksBodyHtml": " x<table><tbody><tr></tr></tbody></table>"
+      }
+    },
+    {
+      "data": "<!doctype html><table>X<style> <tr>x </style> </table>",
+      "errors": [
+        "(1,23): foster-parenting-character"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true,
+            "style": true
+          },
+          "doctype": true,
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "X"
+                  },
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "style",
+                        "children": [
+                          {
+                            "text": " <tr>x ",
+                            "no_escape": true
+                          }
+                        ]
+                      },
+                      {
+                        "text": " "
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body>X<table><style> <tr>x </style> </table></body></html>",
+        "noQuirksBodyHtml": "X<table><style> <tr>x </style> </table>"
+      }
+    },
+    {
+      "data": "<!doctype html><div><table><a>foo</a> <tr><td>bar</td> </tr></table></div>",
+      "errors": [
+        "(1,30): foster-parenting-start-tag",
+        "(1,31): foster-parenting-character",
+        "(1,32): foster-parenting-character",
+        "(1,33): foster-parenting-character",
+        "(1,37): foster-parenting-end-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "div": true,
+            "a": true,
+            "table": true,
+            "tbody": true,
+            "tr": true,
+            "td": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "div",
+                    "children": [
+                      {
+                        "tag": "a",
+                        "children": [
+                          {
+                            "text": "foo"
+                          }
+                        ]
+                      },
+                      {
+                        "tag": "table",
+                        "children": [
+                          {
+                            "text": " "
+                          },
+                          {
+                            "tag": "tbody",
+                            "children": [
+                              {
+                                "tag": "tr",
+                                "children": [
+                                  {
+                                    "tag": "td",
+                                    "children": [
+                                      {
+                                        "text": "bar"
+                                      }
+                                    ]
+                                  },
+                                  {
+                                    "text": " "
+                                  }
+                                ]
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><div><a>foo</a><table> <tbody><tr><td>bar</td> </tr></tbody></table></div></body></html>",
+        "noQuirksBodyHtml": "<div><a>foo</a><table> <tbody><tr><td>bar</td> </tr></tbody></table></div>"
+      }
+    },
+    {
+      "data": "<frame></frame></frame><frameset><frame><frameset><frame></frameset><noframes></frameset><noframes>",
+      "errors": [
+        "(1,7): expected-doctype-but-got-start-tag",
+        "(1,7): unexpected-start-tag-ignored",
+        "(1,15): unexpected-end-tag",
+        "(1,23): unexpected-end-tag",
+        "(1,33): unexpected-start-tag",
+        "(1,99): expected-named-closing-tag-but-got-eof",
+        "(1,99): eof-in-frameset"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "frameset": true,
+            "frame": true,
+            "noframes": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "frameset",
+                "children": [
+                  {
+                    "tag": "frame"
+                  },
+                  {
+                    "tag": "frameset",
+                    "children": [
+                      {
+                        "tag": "frame"
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "noframes",
+                    "children": [
+                      {
+                        "text": "</frameset><noframes>",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><frameset><frame><frameset><frame></frameset><noframes></frameset><noframes></noframes></frameset></html>",
+        "noQuirksBodyHtml": "<noframes></frameset><noframes></noframes>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><object></html>",
+      "errors": [
+        "(1,30): expected-body-in-scope",
+        "(1,30): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "object": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "object"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><object></object></body></html>",
+        "noQuirksBodyHtml": "<object></object>"
+      }
+    }
+  ],
+  "tests16.dat": [
+    {
+      "data": "<!doctype html><script>",
+      "errors": [
+        "(1,23): expected-named-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script"
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head><script></script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script></script>"
+      }
+    },
+    {
+      "data": "<!doctype html><script>a",
+      "errors": [
+        "(1,24): expected-named-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "doctype": true,
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "a",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head><script>a</script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script>a</script>"
+      }
+    },
+    {
+      "data": "<!doctype html><script><",
+      "errors": [
+        "(1,24): expected-named-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "doctype": true,
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "<",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head><script><</script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script><</script>"
+      }
+    },
+    {
+      "data": "<!doctype html><script></",
+      "errors": [
+        "(1,25): expected-named-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "doctype": true,
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "</",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head><script></</script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script></</script>"
+      }
+    },
+    {
+      "data": "<!doctype html><script></S",
+      "errors": [
+        "(1,26): expected-named-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "doctype": true,
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "</S",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head><script></S</script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script></S</script>"
+      }
+    },
+    {
+      "data": "<!doctype html><script></SC",
+      "errors": [
+        "(1,27): expected-named-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "doctype": true,
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "</SC",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head><script></SC</script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script></SC</script>"
+      }
+    },
+    {
+      "data": "<!doctype html><script></SCR",
+      "errors": [
+        "(1,28): expected-named-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "doctype": true,
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "</SCR",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head><script></SCR</script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script></SCR</script>"
+      }
+    },
+    {
+      "data": "<!doctype html><script></SCRI",
+      "errors": [
+        "(1,29): expected-named-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "doctype": true,
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "</SCRI",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head><script></SCRI</script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script></SCRI</script>"
+      }
+    },
+    {
+      "data": "<!doctype html><script></SCRIP",
+      "errors": [
+        "(1,30): expected-named-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "doctype": true,
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "</SCRIP",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head><script></SCRIP</script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script></SCRIP</script>"
+      }
+    },
+    {
+      "data": "<!doctype html><script></SCRIPT",
+      "errors": [
+        "(1,31): expected-named-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "doctype": true,
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "</SCRIPT",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head><script></SCRIPT</script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script></SCRIPT</script>"
+      }
+    },
+    {
+      "data": "<!doctype html><script></SCRIPT ",
+      "errors": [
+        "(1,32): expected-attribute-name-but-got-eof",
+        "(1,32): expected-named-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script"
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head><script></script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script></script>"
+      }
+    },
+    {
+      "data": "<!doctype html><script></s",
+      "errors": [
+        "(1,26): expected-named-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "doctype": true,
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "</s",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head><script></s</script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script></s</script>"
+      }
+    },
+    {
+      "data": "<!doctype html><script></sc",
+      "errors": [
+        "(1,27): expected-named-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "doctype": true,
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "</sc",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head><script></sc</script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script></sc</script>"
+      }
+    },
+    {
+      "data": "<!doctype html><script></scr",
+      "errors": [
+        "(1,28): expected-named-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "doctype": true,
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "</scr",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head><script></scr</script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script></scr</script>"
+      }
+    },
+    {
+      "data": "<!doctype html><script></scri",
+      "errors": [
+        "(1,29): expected-named-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "doctype": true,
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "</scri",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head><script></scri</script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script></scri</script>"
+      }
+    },
+    {
+      "data": "<!doctype html><script></scrip",
+      "errors": [
+        "(1,30): expected-named-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "doctype": true,
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "</scrip",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head><script></scrip</script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script></scrip</script>"
+      }
+    },
+    {
+      "data": "<!doctype html><script></script",
+      "errors": [
+        "(1,31): expected-named-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "doctype": true,
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "</script",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head><script></script</script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script></script</script>"
+      }
+    },
+    {
+      "data": "<!doctype html><script></script ",
+      "errors": [
+        "(1,32): expected-attribute-name-but-got-eof",
+        "(1,32): expected-named-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script"
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head><script></script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script></script>"
+      }
+    },
+    {
+      "data": "<!doctype html><script><!",
+      "errors": [
+        "(1,25): expected-script-data-but-got-eof",
+        "(1,25): expected-named-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "doctype": true,
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "<!",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head><script><!</script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script><!</script>"
+      }
+    },
+    {
+      "data": "<!doctype html><script><!a",
+      "errors": [
+        "(1,26): expected-named-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "doctype": true,
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "<!a",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head><script><!a</script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script><!a</script>"
+      }
+    },
+    {
+      "data": "<!doctype html><script><!-",
+      "errors": [
+        "(1,26): expected-named-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "doctype": true,
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "<!-",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head><script><!-</script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script><!-</script>"
+      }
+    },
+    {
+      "data": "<!doctype html><script><!-a",
+      "errors": [
+        "(1,27): expected-named-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "doctype": true,
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "<!-a",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head><script><!-a</script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script><!-a</script>"
+      }
+    },
+    {
+      "data": "<!doctype html><script><!--",
+      "errors": [
+        "(1,27): expected-named-closing-tag-but-got-eof",
+        "(1,27): unexpected-eof-in-text-mode"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "doctype": true,
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "<!--",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head><script><!--</script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script><!--</script>"
+      }
+    },
+    {
+      "data": "<!doctype html><script><!--a",
+      "errors": [
+        "(1,28): expected-named-closing-tag-but-got-eof",
+        "(1,28): unexpected-eof-in-text-mode"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "doctype": true,
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "<!--a",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head><script><!--a</script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script><!--a</script>"
+      }
+    },
+    {
+      "data": "<!doctype html><script><!--<",
+      "errors": [
+        "(1,28): expected-named-closing-tag-but-got-eof",
+        "(1,28): unexpected-eof-in-text-mode"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "doctype": true,
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "<!--<",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head><script><!--<</script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script><!--<</script>"
+      }
+    },
+    {
+      "data": "<!doctype html><script><!--<a",
+      "errors": [
+        "(1,29): expected-named-closing-tag-but-got-eof",
+        "(1,29): unexpected-eof-in-text-mode"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "doctype": true,
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "<!--<a",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head><script><!--<a</script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script><!--<a</script>"
+      }
+    },
+    {
+      "data": "<!doctype html><script><!--</",
+      "errors": [
+        "(1,29): expected-named-closing-tag-but-got-eof",
+        "(1,29): unexpected-eof-in-text-mode"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "doctype": true,
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "<!--</",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head><script><!--</</script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script><!--</</script>"
+      }
+    },
+    {
+      "data": "<!doctype html><script><!--</script",
+      "errors": [
+        "(1,35): expected-named-closing-tag-but-got-eof",
+        "(1,35): unexpected-eof-in-text-mode"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "doctype": true,
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "<!--</script",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head><script><!--</script</script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script><!--</script</script>"
+      }
+    },
+    {
+      "data": "<!doctype html><script><!--</script ",
+      "errors": [
+        "(1,36): expected-attribute-name-but-got-eof",
+        "(1,36): expected-named-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "doctype": true,
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "<!--",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head><script><!--</script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script><!--</script>"
+      }
+    },
+    {
+      "data": "<!doctype html><script><!--<s",
+      "errors": [
+        "(1,29): expected-named-closing-tag-but-got-eof",
+        "(1,29): unexpected-eof-in-text-mode"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "doctype": true,
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "<!--<s",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head><script><!--<s</script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script><!--<s</script>"
+      }
+    },
+    {
+      "data": "<!doctype html><script><!--<script",
+      "errors": [
+        "(1,34): expected-named-closing-tag-but-got-eof",
+        "(1,34): unexpected-eof-in-text-mode"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "doctype": true,
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "<!--<script",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head><script><!--<script</script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script><!--<script</script>"
+      }
+    },
+    {
+      "data": "<!doctype html><script><!--<script ",
+      "errors": [
+        "(1,35): eof-in-script-in-script",
+        "(1,35): expected-named-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "doctype": true,
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "<!--<script ",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head><script><!--<script </script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script><!--<script </script>"
+      }
+    },
+    {
+      "data": "<!doctype html><script><!--<script <",
+      "errors": [
+        "(1,36): eof-in-script-in-script",
+        "(1,36): expected-named-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "doctype": true,
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "<!--<script <",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head><script><!--<script <</script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script><!--<script <</script>"
+      }
+    },
+    {
+      "data": "<!doctype html><script><!--<script <a",
+      "errors": [
+        "(1,37): eof-in-script-in-script",
+        "(1,37): expected-named-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "doctype": true,
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "<!--<script <a",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head><script><!--<script <a</script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script><!--<script <a</script>"
+      }
+    },
+    {
+      "data": "<!doctype html><script><!--<script </",
+      "errors": [
+        "(1,37): eof-in-script-in-script",
+        "(1,37): expected-named-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "doctype": true,
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "<!--<script </",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head><script><!--<script </</script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script><!--<script </</script>"
+      }
+    },
+    {
+      "data": "<!doctype html><script><!--<script </s",
+      "errors": [
+        "(1,38): eof-in-script-in-script",
+        "(1,38): expected-named-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "doctype": true,
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "<!--<script </s",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head><script><!--<script </s</script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script><!--<script </s</script>"
+      }
+    },
+    {
+      "data": "<!doctype html><script><!--<script </script",
+      "errors": [
+        "(1,43): eof-in-script-in-script",
+        "(1,43): expected-named-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "doctype": true,
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "<!--<script </script",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head><script><!--<script </script</script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script><!--<script </script</script>"
+      }
+    },
+    {
+      "data": "<!doctype html><script><!--<script </scripta",
+      "errors": [
+        "(1,44): eof-in-script-in-script",
+        "(1,44): expected-named-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "doctype": true,
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "<!--<script </scripta",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head><script><!--<script </scripta</script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script><!--<script </scripta</script>"
+      }
+    },
+    {
+      "data": "<!doctype html><script><!--<script </script ",
+      "errors": [
+        "(1,44): expected-named-closing-tag-but-got-eof",
+        "(1,44): unexpected-eof-in-text-mode"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "doctype": true,
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "<!--<script </script ",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head><script><!--<script </script </script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script><!--<script </script </script>"
+      }
+    },
+    {
+      "data": "<!doctype html><script><!--<script </script>",
+      "errors": [
+        "(1,44): expected-named-closing-tag-but-got-eof",
+        "(1,44): unexpected-eof-in-text-mode"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "doctype": true,
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "<!--<script </script>",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head><script><!--<script </script></script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script><!--<script </script></script>"
+      }
+    },
+    {
+      "data": "<!doctype html><script><!--<script </script/",
+      "errors": [
+        "(1,44): expected-named-closing-tag-but-got-eof",
+        "(1,44): unexpected-eof-in-text-mode"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "doctype": true,
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "<!--<script </script/",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head><script><!--<script </script/</script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script><!--<script </script/</script>"
+      }
+    },
+    {
+      "data": "<!doctype html><script><!--<script </script <",
+      "errors": [
+        "(1,45): expected-named-closing-tag-but-got-eof",
+        "(1,45): unexpected-eof-in-text-mode"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "doctype": true,
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "<!--<script </script <",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head><script><!--<script </script <</script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script><!--<script </script <</script>"
+      }
+    },
+    {
+      "data": "<!doctype html><script><!--<script </script <a",
+      "errors": [
+        "(1,46): expected-named-closing-tag-but-got-eof",
+        "(1,46): unexpected-eof-in-text-mode"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "doctype": true,
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "<!--<script </script <a",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head><script><!--<script </script <a</script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script><!--<script </script <a</script>"
+      }
+    },
+    {
+      "data": "<!doctype html><script><!--<script </script </",
+      "errors": [
+        "(1,46): expected-named-closing-tag-but-got-eof",
+        "(1,46): unexpected-eof-in-text-mode"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "doctype": true,
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "<!--<script </script </",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head><script><!--<script </script </</script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script><!--<script </script </</script>"
+      }
+    },
+    {
+      "data": "<!doctype html><script><!--<script </script </script",
+      "errors": [
+        "(1,52): expected-named-closing-tag-but-got-eof",
+        "(1,52): unexpected-eof-in-text-mode"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "doctype": true,
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "<!--<script </script </script",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head><script><!--<script </script </script</script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script><!--<script </script </script</script>"
+      }
+    },
+    {
+      "data": "<!doctype html><script><!--<script </script </script ",
+      "errors": [
+        "(1,53): expected-attribute-name-but-got-eof",
+        "(1,53): expected-named-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "doctype": true,
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "<!--<script </script ",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head><script><!--<script </script </script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script><!--<script </script </script>"
+      }
+    },
+    {
+      "data": "<!doctype html><script><!--<script </script </script/",
+      "errors": [
+        "(1,53): unexpected-EOF-after-solidus-in-tag",
+        "(1,53): expected-named-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "doctype": true,
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "<!--<script </script ",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head><script><!--<script </script </script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script><!--<script </script </script>"
+      }
+    },
+    {
+      "data": "<!doctype html><script><!--<script </script </script>",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "doctype": true,
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "<!--<script </script ",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head><script><!--<script </script </script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script><!--<script </script </script>"
+      }
+    },
+    {
+      "data": "<!doctype html><script><!--<script -",
+      "errors": [
+        "(1,36): eof-in-script-in-script",
+        "(1,36): expected-named-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "doctype": true,
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "<!--<script -",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head><script><!--<script -</script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script><!--<script -</script>"
+      }
+    },
+    {
+      "data": "<!doctype html><script><!--<script -a",
+      "errors": [
+        "(1,37): eof-in-script-in-script",
+        "(1,37): expected-named-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "doctype": true,
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "<!--<script -a",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head><script><!--<script -a</script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script><!--<script -a</script>"
+      }
+    },
+    {
+      "data": "<!doctype html><script><!--<script -<",
+      "errors": [
+        "(1,37): eof-in-script-in-script",
+        "(1,37): expected-named-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "doctype": true,
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "<!--<script -<",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head><script><!--<script -<</script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script><!--<script -<</script>"
+      }
+    },
+    {
+      "data": "<!doctype html><script><!--<script --",
+      "errors": [
+        "(1,37): eof-in-script-in-script",
+        "(1,37): expected-named-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "doctype": true,
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "<!--<script --",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head><script><!--<script --</script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script><!--<script --</script>"
+      }
+    },
+    {
+      "data": "<!doctype html><script><!--<script --a",
+      "errors": [
+        "(1,38): eof-in-script-in-script",
+        "(1,38): expected-named-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "doctype": true,
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "<!--<script --a",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head><script><!--<script --a</script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script><!--<script --a</script>"
+      }
+    },
+    {
+      "data": "<!doctype html><script><!--<script --<",
+      "errors": [
+        "(1,38): eof-in-script-in-script",
+        "(1,38): expected-named-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "doctype": true,
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "<!--<script --<",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head><script><!--<script --<</script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script><!--<script --<</script>"
+      }
+    },
+    {
+      "data": "<!doctype html><script><!--<script -->",
+      "errors": [
+        "(1,38): expected-named-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "doctype": true,
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "<!--<script -->",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head><script><!--<script --></script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script><!--<script --></script>"
+      }
+    },
+    {
+      "data": "<!doctype html><script><!--<script --><",
+      "errors": [
+        "(1,39): expected-named-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "doctype": true,
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "<!--<script --><",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head><script><!--<script --><</script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script><!--<script --><</script>"
+      }
+    },
+    {
+      "data": "<!doctype html><script><!--<script --></",
+      "errors": [
+        "(1,40): expected-named-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "doctype": true,
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "<!--<script --></",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head><script><!--<script --></</script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script><!--<script --></</script>"
+      }
+    },
+    {
+      "data": "<!doctype html><script><!--<script --></script",
+      "errors": [
+        "(1,46): expected-named-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "doctype": true,
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "<!--<script --></script",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head><script><!--<script --></script</script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script><!--<script --></script</script>"
+      }
+    },
+    {
+      "data": "<!doctype html><script><!--<script --></script ",
+      "errors": [
+        "(1,47): expected-attribute-name-but-got-eof",
+        "(1,47): expected-named-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "doctype": true,
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "<!--<script -->",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head><script><!--<script --></script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script><!--<script --></script>"
+      }
+    },
+    {
+      "data": "<!doctype html><script><!--<script --></script/",
+      "errors": [
+        "(1,47): unexpected-EOF-after-solidus-in-tag",
+        "(1,47): expected-named-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "doctype": true,
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "<!--<script -->",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head><script><!--<script --></script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script><!--<script --></script>"
+      }
+    },
+    {
+      "data": "<!doctype html><script><!--<script --></script>",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "doctype": true,
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "<!--<script -->",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head><script><!--<script --></script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script><!--<script --></script>"
+      }
+    },
+    {
+      "data": "<!doctype html><script><!--<script><\\/script>--></script>",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "doctype": true,
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "<!--<script><\\/script>-->",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head><script><!--<script><\\/script>--></script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script><!--<script><\\/script>--></script>"
+      }
+    },
+    {
+      "data": "<!doctype html><script><!--<script></scr'+'ipt>--></script>",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "doctype": true,
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "<!--<script></scr'+'ipt>-->",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head><script><!--<script></scr'+'ipt>--></script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script><!--<script></scr'+'ipt>--></script>"
+      }
+    },
+    {
+      "data": "<!doctype html><script><!--<script></script><script></script></script>",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "doctype": true,
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "<!--<script></script><script></script>",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head><script><!--<script></script><script></script></script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script><!--<script></script><script></script></script>"
+      }
+    },
+    {
+      "data": "<!doctype html><script><!--<script></script><script></script>--><!--</script>",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "doctype": true,
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "<!--<script></script><script></script>--><!--",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head><script><!--<script></script><script></script>--><!--</script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script><!--<script></script><script></script>--><!--</script>"
+      }
+    },
+    {
+      "data": "<!doctype html><script><!--<script></script><script></script>-- ></script>",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "doctype": true,
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "<!--<script></script><script></script>-- >",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head><script><!--<script></script><script></script>-- ></script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script><!--<script></script><script></script>-- ></script>"
+      }
+    },
+    {
+      "data": "<!doctype html><script><!--<script></script><script></script>- -></script>",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "doctype": true,
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "<!--<script></script><script></script>- ->",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head><script><!--<script></script><script></script>- -></script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script><!--<script></script><script></script>- -></script>"
+      }
+    },
+    {
+      "data": "<!doctype html><script><!--<script></script><script></script>- - ></script>",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "doctype": true,
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "<!--<script></script><script></script>- - >",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head><script><!--<script></script><script></script>- - ></script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script><!--<script></script><script></script>- - ></script>"
+      }
+    },
+    {
+      "data": "<!doctype html><script><!--<script></script><script></script>-></script>",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "doctype": true,
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "<!--<script></script><script></script>->",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head><script><!--<script></script><script></script>-></script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script><!--<script></script><script></script>-></script>"
+      }
+    },
+    {
+      "data": "<!doctype html><script><!--<script>--!></script>X",
+      "errors": [
+        "(1,49): expected-named-closing-tag-but-got-eof",
+        "(1,49): unexpected-EOF-in-text-mode"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "doctype": true,
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "<!--<script>--!></script>X",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head><script><!--<script>--!></script>X</script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script><!--<script>--!></script>X</script>"
+      }
+    },
+    {
+      "data": "<!doctype html><script><!--<scr'+'ipt></script>--></script>",
+      "errors": [
+        "(1,59): unexpected-end-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "doctype": true,
+          "no_escape": true,
+          "escaped": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "<!--<scr'+'ipt>",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "-->",
+                    "escaped": true
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head><script><!--<scr'+'ipt></script></head><body>--&gt;</body></html>",
+        "noQuirksBodyHtml": "<script><!--<scr'+'ipt></script>--&gt;"
+      }
+    },
+    {
+      "data": "<!doctype html><script><!--<script></scr'+'ipt></script>X",
+      "errors": [
+        "(1,57): expected-named-closing-tag-but-got-eof",
+        "(1,57): unexpected-eof-in-text-mode"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "doctype": true,
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "<!--<script></scr'+'ipt></script>X",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head><script><!--<script></scr'+'ipt></script>X</script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script><!--<script></scr'+'ipt></script>X</script>"
+      }
+    },
+    {
+      "data": "<!doctype html><style><!--<style></style>--></style>",
+      "errors": [
+        "(1,52): unexpected-end-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "style": true,
+            "body": true
+          },
+          "doctype": true,
+          "no_escape": true,
+          "escaped": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "style",
+                    "children": [
+                      {
+                        "text": "<!--<style>",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "-->",
+                    "escaped": true
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head><style><!--<style></style></head><body>--&gt;</body></html>",
+        "noQuirksBodyHtml": "<style><!--<style></style>--&gt;"
+      }
+    },
+    {
+      "data": "<!doctype html><style><!--</style>X",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "style": true,
+            "body": true
+          },
+          "doctype": true,
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "style",
+                    "children": [
+                      {
+                        "text": "<!--",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "X"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head><style><!--</style></head><body>X</body></html>",
+        "noQuirksBodyHtml": "<style><!--</style>X"
+      }
+    },
+    {
+      "data": "<!doctype html><style><!--...</style>...--></style>",
+      "errors": [
+        "(1,51): unexpected-end-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "style": true,
+            "body": true
+          },
+          "doctype": true,
+          "no_escape": true,
+          "escaped": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "style",
+                    "children": [
+                      {
+                        "text": "<!--...",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "...-->",
+                    "escaped": true
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head><style><!--...</style></head><body>...--&gt;</body></html>",
+        "noQuirksBodyHtml": "<style><!--...</style>...--&gt;"
+      }
+    },
+    {
+      "data": "<!doctype html><style><!--<br><html xmlns:v=\"urn:schemas-microsoft-com:vml\"><!--[if !mso]><style></style>X",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "style": true,
+            "body": true
+          },
+          "doctype": true,
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "style",
+                    "children": [
+                      {
+                        "text": "<!--<br><html xmlns:v=\"urn:schemas-microsoft-com:vml\"><!--[if !mso]><style>",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "X"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head><style><!--<br><html xmlns:v=\"urn:schemas-microsoft-com:vml\"><!--[if !mso]><style></style></head><body>X</body></html>",
+        "noQuirksBodyHtml": "<style><!--<br><html xmlns:v=\"urn:schemas-microsoft-com:vml\"><!--[if !mso]><style></style>X"
+      }
+    },
+    {
+      "data": "<!doctype html><style><!--...<style><!--...--!></style>--></style>",
+      "errors": [
+        "(1,66): unexpected-end-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "style": true,
+            "body": true
+          },
+          "doctype": true,
+          "no_escape": true,
+          "escaped": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "style",
+                    "children": [
+                      {
+                        "text": "<!--...<style><!--...--!>",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "-->",
+                    "escaped": true
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head><style><!--...<style><!--...--!></style></head><body>--&gt;</body></html>",
+        "noQuirksBodyHtml": "<style><!--...<style><!--...--!></style>--&gt;"
+      }
+    },
+    {
+      "data": "<!doctype html><style><!--...</style><!-- --><style>@import ...</style>",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "style": true,
+            "body": true
+          },
+          "doctype": true,
+          "no_escape": true,
+          "comment": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "style",
+                    "children": [
+                      {
+                        "text": "<!--...",
+                        "no_escape": true
+                      }
+                    ]
+                  },
+                  {
+                    "comment": " "
+                  },
+                  {
+                    "tag": "style",
+                    "children": [
+                      {
+                        "text": "@import ...",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head><style><!--...</style><!-- --><style>@import ...</style></head><body></body></html>",
+        "noQuirksBodyHtml": "<style><!--...</style><!-- --><style>@import ...</style>"
+      }
+    },
+    {
+      "data": "<!doctype html><style>...<style><!--...</style><!-- --></style>",
+      "errors": [
+        "(1,63): unexpected-end-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "style": true,
+            "body": true
+          },
+          "doctype": true,
+          "no_escape": true,
+          "comment": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "style",
+                    "children": [
+                      {
+                        "text": "...<style><!--...",
+                        "no_escape": true
+                      }
+                    ]
+                  },
+                  {
+                    "comment": " "
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head><style>...<style><!--...</style><!-- --></head><body></body></html>",
+        "noQuirksBodyHtml": "<style>...<style><!--...</style><!-- -->"
+      }
+    },
+    {
+      "data": "<!doctype html><style>...<!--[if IE]><style>...</style>X",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "style": true,
+            "body": true
+          },
+          "doctype": true,
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "style",
+                    "children": [
+                      {
+                        "text": "...<!--[if IE]><style>...",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "X"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head><style>...<!--[if IE]><style>...</style></head><body>X</body></html>",
+        "noQuirksBodyHtml": "<style>...<!--[if IE]><style>...</style>X"
+      }
+    },
+    {
+      "data": "<!doctype html><title><!--<title></title>--></title>",
+      "errors": [
+        "(1,52): unexpected-end-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "title": true,
+            "body": true
+          },
+          "doctype": true,
+          "escaped": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "title",
+                    "children": [
+                      {
+                        "text": "<!--<title>",
+                        "escaped": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "-->",
+                    "escaped": true
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head><title>&lt;!--&lt;title&gt;</title></head><body>--&gt;</body></html>",
+        "noQuirksBodyHtml": "<title>&lt;!--&lt;title&gt;</title>--&gt;"
+      }
+    },
+    {
+      "data": "<!doctype html><title>&lt;/title></title>",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "title": true,
+            "body": true
+          },
+          "doctype": true,
+          "escaped": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "title",
+                    "children": [
+                      {
+                        "text": "</title>",
+                        "escaped": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head><title>&lt;/title&gt;</title></head><body></body></html>",
+        "noQuirksBodyHtml": "<title>&lt;/title&gt;</title>"
+      }
+    },
+    {
+      "data": "<!doctype html><title>foo/title><link></head><body>X",
+      "errors": [
+        "(1,52): expected-named-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "title": true,
+            "body": true
+          },
+          "doctype": true,
+          "escaped": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "title",
+                    "children": [
+                      {
+                        "text": "foo/title><link></head><body>X",
+                        "escaped": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head><title>foo/title&gt;&lt;link&gt;&lt;/head&gt;&lt;body&gt;X</title></head><body></body></html>",
+        "noQuirksBodyHtml": "<title>foo/title&gt;&lt;link&gt;&lt;/head&gt;&lt;body&gt;X</title>"
+      }
+    },
+    {
+      "data": "<!doctype html><noscript><!--<noscript></noscript>--></noscript>",
+      "errors": [
+        "(1,64): unexpected-end-tag"
+      ],
+      "script": "on",
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "noscript": true,
+            "body": true
+          },
+          "doctype": true,
+          "no_escape": true,
+          "escaped": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "noscript",
+                    "children": [
+                      {
+                        "text": "<!--<noscript>",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "-->",
+                    "escaped": true
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head><noscript><!--<noscript></noscript></head><body>--&gt;</body></html>",
+        "noQuirksBodyHtml": "<noscript><!--<noscript></noscript>--></noscript>"
+      }
+    },
+    {
+      "data": "<!doctype html><noscript><!--<noscript></noscript>--></noscript>",
+      "errors": [],
+      "script": "off",
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "noscript": true,
+            "body": true
+          },
+          "doctype": true,
+          "comment": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "noscript",
+                    "children": [
+                      {
+                        "comment": "<noscript></noscript>"
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head><noscript><!--<noscript></noscript>--></noscript></head><body></body></html>",
+        "noQuirksBodyHtml": "<noscript><!--<noscript></noscript>--></noscript>"
+      }
+    },
+    {
+      "data": "<!doctype html><noscript><!--</noscript>X<noscript>--></noscript>",
+      "errors": [],
+      "script": "on",
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "noscript": true,
+            "body": true
+          },
+          "doctype": true,
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "noscript",
+                    "children": [
+                      {
+                        "text": "<!--",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "X"
+                  },
+                  {
+                    "tag": "noscript",
+                    "children": [
+                      {
+                        "text": "-->",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head><noscript><!--</noscript></head><body>X<noscript>--></noscript></body></html>",
+        "noQuirksBodyHtml": "<noscript><!--</noscript>X<noscript>--></noscript>"
+      }
+    },
+    {
+      "data": "<!doctype html><noscript><!--</noscript>X<noscript>--></noscript>",
+      "errors": [],
+      "script": "off",
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "noscript": true,
+            "body": true
+          },
+          "doctype": true,
+          "comment": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "noscript",
+                    "children": [
+                      {
+                        "comment": "</noscript>X<noscript>"
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head><noscript><!--</noscript>X<noscript>--></noscript></head><body></body></html>",
+        "noQuirksBodyHtml": "<noscript><!--</noscript>X<noscript>--></noscript>"
+      }
+    },
+    {
+      "data": "<!doctype html><noscript><iframe></noscript>X",
+      "errors": [],
+      "script": "on",
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "noscript": true,
+            "body": true
+          },
+          "doctype": true,
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "noscript",
+                    "children": [
+                      {
+                        "text": "<iframe>",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "X"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head><noscript><iframe></noscript></head><body>X</body></html>",
+        "noQuirksBodyHtml": "<noscript><iframe></noscript>X</iframe></noscript>"
+      }
+    },
+    {
+      "data": "<!doctype html><noscript><iframe></noscript>X",
+      "errors": [
+        " * (1,34) unexpected token in head noscript",
+        " * (1,46) unexpected EOF"
+      ],
+      "script": "off",
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "noscript": true,
+            "body": true,
+            "iframe": true
+          },
+          "doctype": true,
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "noscript"
+                  }
+                ]
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "iframe",
+                    "children": [
+                      {
+                        "text": "</noscript>X",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head><noscript></noscript></head><body><iframe></noscript>X</iframe></body></html>",
+        "noQuirksBodyHtml": "<noscript><iframe></noscript>X</iframe></noscript>"
+      }
+    },
+    {
+      "data": "<!doctype html><noframes><!--<noframes></noframes>--></noframes>",
+      "errors": [
+        "(1,64): unexpected-end-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "noframes": true,
+            "body": true
+          },
+          "doctype": true,
+          "no_escape": true,
+          "escaped": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "noframes",
+                    "children": [
+                      {
+                        "text": "<!--<noframes>",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "-->",
+                    "escaped": true
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head><noframes><!--<noframes></noframes></head><body>--&gt;</body></html>",
+        "noQuirksBodyHtml": "<noframes><!--<noframes></noframes>--&gt;"
+      }
+    },
+    {
+      "data": "<!doctype html><noframes><body><script><!--...</script></body></noframes></html>",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "noframes": true,
+            "body": true
+          },
+          "doctype": true,
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "noframes",
+                    "children": [
+                      {
+                        "text": "<body><script><!--...</script></body>",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head><noframes><body><script><!--...</script></body></noframes></head><body></body></html>",
+        "noQuirksBodyHtml": "<noframes><body><script><!--...</script></body></noframes>"
+      }
+    },
+    {
+      "data": "<!doctype html><textarea><!--<textarea></textarea>--></textarea>",
+      "errors": [
+        "(1,64): unexpected-end-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "textarea": true
+          },
+          "doctype": true,
+          "escaped": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "textarea",
+                    "children": [
+                      {
+                        "text": "<!--<textarea>",
+                        "escaped": true
+                      }
+                    ]
+                  },
+                  {
+                    "text": "-->",
+                    "escaped": true
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><textarea>&lt;!--&lt;textarea&gt;</textarea>--&gt;</body></html>",
+        "noQuirksBodyHtml": "<textarea>&lt;!--&lt;textarea&gt;</textarea>--&gt;"
+      }
+    },
+    {
+      "data": "<!doctype html><textarea>&lt;/textarea></textarea>",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "textarea": true
+          },
+          "doctype": true,
+          "escaped": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "textarea",
+                    "children": [
+                      {
+                        "text": "</textarea>",
+                        "escaped": true
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><textarea>&lt;/textarea&gt;</textarea></body></html>",
+        "noQuirksBodyHtml": "<textarea>&lt;/textarea&gt;</textarea>"
+      }
+    },
+    {
+      "data": "<!doctype html><textarea>&lt;</textarea>",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "textarea": true
+          },
+          "doctype": true,
+          "escaped": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "textarea",
+                    "children": [
+                      {
+                        "text": "<",
+                        "escaped": true
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><textarea>&lt;</textarea></body></html>",
+        "noQuirksBodyHtml": "<textarea>&lt;</textarea>"
+      }
+    },
+    {
+      "data": "<!doctype html><textarea>a&lt;b</textarea>",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "textarea": true
+          },
+          "doctype": true,
+          "escaped": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "textarea",
+                    "children": [
+                      {
+                        "text": "a<b",
+                        "escaped": true
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><textarea>a&lt;b</textarea></body></html>",
+        "noQuirksBodyHtml": "<textarea>a&lt;b</textarea>"
+      }
+    },
+    {
+      "data": "<!doctype html><iframe><!--<iframe></iframe>--></iframe>",
+      "errors": [
+        "(1,56): unexpected-end-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "iframe": true
+          },
+          "doctype": true,
+          "no_escape": true,
+          "escaped": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "iframe",
+                    "children": [
+                      {
+                        "text": "<!--<iframe>",
+                        "no_escape": true
+                      }
+                    ]
+                  },
+                  {
+                    "text": "-->",
+                    "escaped": true
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><iframe><!--<iframe></iframe>--&gt;</body></html>",
+        "noQuirksBodyHtml": "<iframe><!--<iframe></iframe>--&gt;"
+      }
+    },
+    {
+      "data": "<!doctype html><iframe>...<!--X->...<!--/X->...</iframe>",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "iframe": true
+          },
+          "doctype": true,
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "iframe",
+                    "children": [
+                      {
+                        "text": "...<!--X->...<!--/X->...",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><iframe>...<!--X->...<!--/X->...</iframe></body></html>",
+        "noQuirksBodyHtml": "<iframe>...<!--X->...<!--/X->...</iframe>"
+      }
+    },
+    {
+      "data": "<!doctype html><xmp><!--<xmp></xmp>--></xmp>",
+      "errors": [
+        "(1,44): unexpected-end-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "xmp": true
+          },
+          "doctype": true,
+          "no_escape": true,
+          "escaped": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "xmp",
+                    "children": [
+                      {
+                        "text": "<!--<xmp>",
+                        "no_escape": true
+                      }
+                    ]
+                  },
+                  {
+                    "text": "-->",
+                    "escaped": true
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><xmp><!--<xmp></xmp>--&gt;</body></html>",
+        "noQuirksBodyHtml": "<xmp><!--<xmp></xmp>--&gt;"
+      }
+    },
+    {
+      "data": "<!doctype html><noembed><!--<noembed></noembed>--></noembed>",
+      "errors": [
+        "(1,60): unexpected-end-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "noembed": true
+          },
+          "doctype": true,
+          "no_escape": true,
+          "escaped": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "noembed",
+                    "children": [
+                      {
+                        "text": "<!--<noembed>",
+                        "no_escape": true
+                      }
+                    ]
+                  },
+                  {
+                    "text": "-->",
+                    "escaped": true
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><noembed><!--<noembed></noembed>--&gt;</body></html>",
+        "noQuirksBodyHtml": "<noembed><!--<noembed></noembed>--&gt;"
+      }
+    },
+    {
+      "data": "<script>",
+      "errors": [
+        "(1,8): expected-doctype-but-got-start-tag",
+        "(1,8): expected-named-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script"
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><script></script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script></script>"
+      }
+    },
+    {
+      "data": "<script>a",
+      "errors": [
+        "(1,8): expected-doctype-but-got-start-tag",
+        "(1,9): expected-named-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "a",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><script>a</script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script>a</script>"
+      }
+    },
+    {
+      "data": "<script><",
+      "errors": [
+        "(1,8): expected-doctype-but-got-start-tag",
+        "(1,9): expected-named-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "<",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><script><</script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script><</script>"
+      }
+    },
+    {
+      "data": "<script></",
+      "errors": [
+        "(1,8): expected-doctype-but-got-start-tag",
+        "(1,10): expected-named-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "</",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><script></</script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script></</script>"
+      }
+    },
+    {
+      "data": "<script></S",
+      "errors": [
+        "(1,8): expected-doctype-but-got-start-tag",
+        "(1,11): expected-named-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "</S",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><script></S</script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script></S</script>"
+      }
+    },
+    {
+      "data": "<script></SC",
+      "errors": [
+        "(1,8): expected-doctype-but-got-start-tag",
+        "(1,12): expected-named-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "</SC",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><script></SC</script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script></SC</script>"
+      }
+    },
+    {
+      "data": "<script></SCR",
+      "errors": [
+        "(1,8): expected-doctype-but-got-start-tag",
+        "(1,13): expected-named-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "</SCR",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><script></SCR</script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script></SCR</script>"
+      }
+    },
+    {
+      "data": "<script></SCRI",
+      "errors": [
+        "(1,8): expected-doctype-but-got-start-tag",
+        "(1,14): expected-named-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "</SCRI",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><script></SCRI</script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script></SCRI</script>"
+      }
+    },
+    {
+      "data": "<script></SCRIP",
+      "errors": [
+        "(1,8): expected-doctype-but-got-start-tag",
+        "(1,15): expected-named-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "</SCRIP",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><script></SCRIP</script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script></SCRIP</script>"
+      }
+    },
+    {
+      "data": "<script></SCRIPT",
+      "errors": [
+        "(1,8): expected-doctype-but-got-start-tag",
+        "(1,16): expected-named-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "</SCRIPT",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><script></SCRIPT</script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script></SCRIPT</script>"
+      }
+    },
+    {
+      "data": "<script></SCRIPT ",
+      "errors": [
+        "(1,8): expected-doctype-but-got-start-tag",
+        "(1,17): expected-attribute-name-but-got-eof",
+        "(1,17): expected-named-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script"
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><script></script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script></script>"
+      }
+    },
+    {
+      "data": "<script></s",
+      "errors": [
+        "(1,8): expected-doctype-but-got-start-tag",
+        "(1,11): expected-named-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "</s",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><script></s</script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script></s</script>"
+      }
+    },
+    {
+      "data": "<script></sc",
+      "errors": [
+        "(1,8): expected-doctype-but-got-start-tag",
+        "(1,12): expected-named-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "</sc",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><script></sc</script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script></sc</script>"
+      }
+    },
+    {
+      "data": "<script></scr",
+      "errors": [
+        "(1,8): expected-doctype-but-got-start-tag",
+        "(1,13): expected-named-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "</scr",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><script></scr</script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script></scr</script>"
+      }
+    },
+    {
+      "data": "<script></scri",
+      "errors": [
+        "(1,8): expected-doctype-but-got-start-tag",
+        "(1,14): expected-named-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "</scri",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><script></scri</script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script></scri</script>"
+      }
+    },
+    {
+      "data": "<script></scrip",
+      "errors": [
+        "(1,8): expected-doctype-but-got-start-tag",
+        "(1,15): expected-named-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "</scrip",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><script></scrip</script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script></scrip</script>"
+      }
+    },
+    {
+      "data": "<script></script",
+      "errors": [
+        "(1,8): expected-doctype-but-got-start-tag",
+        "(1,16): expected-named-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "</script",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><script></script</script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script></script</script>"
+      }
+    },
+    {
+      "data": "<script></script ",
+      "errors": [
+        "(1,8): expected-doctype-but-got-start-tag",
+        "(1,17): expected-attribute-name-but-got-eof",
+        "(1,17): expected-named-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script"
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><script></script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script></script>"
+      }
+    },
+    {
+      "data": "<script><!",
+      "errors": [
+        "(1,8): expected-doctype-but-got-start-tag",
+        "(1,10): expected-script-data-but-got-eof",
+        "(1,10): expected-named-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "<!",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><script><!</script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script><!</script>"
+      }
+    },
+    {
+      "data": "<script><!a",
+      "errors": [
+        "(1,8): expected-doctype-but-got-start-tag",
+        "(1,11): expected-named-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "<!a",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><script><!a</script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script><!a</script>"
+      }
+    },
+    {
+      "data": "<script><!-",
+      "errors": [
+        "(1,8): expected-doctype-but-got-start-tag",
+        "(1,11): expected-named-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "<!-",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><script><!-</script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script><!-</script>"
+      }
+    },
+    {
+      "data": "<script><!-a",
+      "errors": [
+        "(1,8): expected-doctype-but-got-start-tag",
+        "(1,12): expected-named-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "<!-a",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><script><!-a</script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script><!-a</script>"
+      }
+    },
+    {
+      "data": "<script><!--",
+      "errors": [
+        "(1,8): expected-doctype-but-got-start-tag",
+        "(1,12): expected-named-closing-tag-but-got-eof",
+        "(1,12): unexpected-eof-in-text-mode"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "<!--",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><script><!--</script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script><!--</script>"
+      }
+    },
+    {
+      "data": "<script><!--a",
+      "errors": [
+        "(1,8): expected-doctype-but-got-start-tag",
+        "(1,13): expected-named-closing-tag-but-got-eof",
+        "(1,13): unexpected-eof-in-text-mode"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "<!--a",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><script><!--a</script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script><!--a</script>"
+      }
+    },
+    {
+      "data": "<script><!--<",
+      "errors": [
+        "(1,8): expected-doctype-but-got-start-tag",
+        "(1,13): expected-named-closing-tag-but-got-eof",
+        "(1,13): unexpected-eof-in-text-mode"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "<!--<",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><script><!--<</script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script><!--<</script>"
+      }
+    },
+    {
+      "data": "<script><!--<a",
+      "errors": [
+        "(1,8): expected-doctype-but-got-start-tag",
+        "(1,14): expected-named-closing-tag-but-got-eof",
+        "(1,14): unexpected-eof-in-text-mode"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "<!--<a",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><script><!--<a</script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script><!--<a</script>"
+      }
+    },
+    {
+      "data": "<script><!--</",
+      "errors": [
+        "(1,8): expected-doctype-but-got-start-tag",
+        "(1,14): expected-named-closing-tag-but-got-eof",
+        "(1,14): unexpected-eof-in-text-mode"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "<!--</",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><script><!--</</script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script><!--</</script>"
+      }
+    },
+    {
+      "data": "<script><!--</script",
+      "errors": [
+        "(1,8): expected-doctype-but-got-start-tag",
+        "(1,20): expected-named-closing-tag-but-got-eof",
+        "(1,20): unexpected-eof-in-text-mode"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "<!--</script",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><script><!--</script</script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script><!--</script</script>"
+      }
+    },
+    {
+      "data": "<script><!--</script ",
+      "errors": [
+        "(1,8): expected-doctype-but-got-start-tag",
+        "(1,21): expected-attribute-name-but-got-eof",
+        "(1,21): expected-named-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "<!--",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><script><!--</script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script><!--</script>"
+      }
+    },
+    {
+      "data": "<script><!--<s",
+      "errors": [
+        "(1,8): expected-doctype-but-got-start-tag",
+        "(1,14): expected-named-closing-tag-but-got-eof",
+        "(1,14): unexpected-eof-in-text-mode"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "<!--<s",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><script><!--<s</script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script><!--<s</script>"
+      }
+    },
+    {
+      "data": "<script><!--<script",
+      "errors": [
+        "(1,8): expected-doctype-but-got-start-tag",
+        "(1,19): expected-named-closing-tag-but-got-eof",
+        "(1,19): unexpected-eof-in-text-mode"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "<!--<script",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><script><!--<script</script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script><!--<script</script>"
+      }
+    },
+    {
+      "data": "<script><!--<script ",
+      "errors": [
+        "(1,8): expected-doctype-but-got-start-tag",
+        "(1,20): eof-in-script-in-script",
+        "(1,20): expected-named-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "<!--<script ",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><script><!--<script </script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script><!--<script </script>"
+      }
+    },
+    {
+      "data": "<script><!--<script <",
+      "errors": [
+        "(1,8): expected-doctype-but-got-start-tag",
+        "(1,21): eof-in-script-in-script",
+        "(1,21): expected-named-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "<!--<script <",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><script><!--<script <</script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script><!--<script <</script>"
+      }
+    },
+    {
+      "data": "<script><!--<script <a",
+      "errors": [
+        "(1,8): expected-doctype-but-got-start-tag",
+        "(1,22): eof-in-script-in-script",
+        "(1,22): expected-named-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "<!--<script <a",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><script><!--<script <a</script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script><!--<script <a</script>"
+      }
+    },
+    {
+      "data": "<script><!--<script </",
+      "errors": [
+        "(1,8): expected-doctype-but-got-start-tag",
+        "(1,22): eof-in-script-in-script",
+        "(1,22): expected-named-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "<!--<script </",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><script><!--<script </</script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script><!--<script </</script>"
+      }
+    },
+    {
+      "data": "<script><!--<script </s",
+      "errors": [
+        "(1,8): expected-doctype-but-got-start-tag",
+        "(1,23): eof-in-script-in-script",
+        "(1,23): expected-named-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "<!--<script </s",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><script><!--<script </s</script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script><!--<script </s</script>"
+      }
+    },
+    {
+      "data": "<script><!--<script </script",
+      "errors": [
+        "(1,8): expected-doctype-but-got-start-tag",
+        "(1,28): eof-in-script-in-script",
+        "(1,28): expected-named-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "<!--<script </script",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><script><!--<script </script</script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script><!--<script </script</script>"
+      }
+    },
+    {
+      "data": "<script><!--<script </scripta",
+      "errors": [
+        "(1,8): expected-doctype-but-got-start-tag",
+        "(1,29): eof-in-script-in-script",
+        "(1,29): expected-named-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "<!--<script </scripta",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><script><!--<script </scripta</script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script><!--<script </scripta</script>"
+      }
+    },
+    {
+      "data": "<script><!--<script </script ",
+      "errors": [
+        "(1,8): expected-doctype-but-got-start-tag",
+        "(1,29): expected-named-closing-tag-but-got-eof",
+        "(1,29): unexpected-eof-in-text-mode"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "<!--<script </script ",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><script><!--<script </script </script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script><!--<script </script </script>"
+      }
+    },
+    {
+      "data": "<script><!--<script </script>",
+      "errors": [
+        "(1,8): expected-doctype-but-got-start-tag",
+        "(1,29): expected-named-closing-tag-but-got-eof",
+        "(1,29): unexpected-eof-in-text-mode"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "<!--<script </script>",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><script><!--<script </script></script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script><!--<script </script></script>"
+      }
+    },
+    {
+      "data": "<script><!--<script </script/",
+      "errors": [
+        "(1,8): expected-doctype-but-got-start-tag",
+        "(1,29): expected-named-closing-tag-but-got-eof",
+        "(1,29): unexpected-eof-in-text-mode"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "<!--<script </script/",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><script><!--<script </script/</script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script><!--<script </script/</script>"
+      }
+    },
+    {
+      "data": "<script><!--<script </script <",
+      "errors": [
+        "(1,8): expected-doctype-but-got-start-tag",
+        "(1,30): expected-named-closing-tag-but-got-eof",
+        "(1,30): unexpected-eof-in-text-mode"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "<!--<script </script <",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><script><!--<script </script <</script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script><!--<script </script <</script>"
+      }
+    },
+    {
+      "data": "<script><!--<script </script <a",
+      "errors": [
+        "(1,8): expected-doctype-but-got-start-tag",
+        "(1,31): expected-named-closing-tag-but-got-eof",
+        "(1,31): unexpected-eof-in-text-mode"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "<!--<script </script <a",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><script><!--<script </script <a</script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script><!--<script </script <a</script>"
+      }
+    },
+    {
+      "data": "<script><!--<script </script </",
+      "errors": [
+        "(1,8): expected-doctype-but-got-start-tag",
+        "(1,31): expected-named-closing-tag-but-got-eof",
+        "(1,31): unexpected-eof-in-text-mode"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "<!--<script </script </",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><script><!--<script </script </</script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script><!--<script </script </</script>"
+      }
+    },
+    {
+      "data": "<script><!--<script </script </script",
+      "errors": [
+        "(1,8): expected-doctype-but-got-start-tag",
+        "(1,37): expected-named-closing-tag-but-got-eof",
+        "(1,37): unexpected-eof-in-text-mode"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "<!--<script </script </script",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><script><!--<script </script </script</script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script><!--<script </script </script</script>"
+      }
+    },
+    {
+      "data": "<script><!--<script </script </script ",
+      "errors": [
+        "(1,8): expected-doctype-but-got-start-tag",
+        "(1,38): expected-attribute-name-but-got-eof",
+        "(1,38): expected-named-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "<!--<script </script ",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><script><!--<script </script </script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script><!--<script </script </script>"
+      }
+    },
+    {
+      "data": "<script><!--<script </script </script/",
+      "errors": [
+        "(1,8): expected-doctype-but-got-start-tag",
+        "(1,38): unexpected-EOF-after-solidus-in-tag",
+        "(1,38): expected-named-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "<!--<script </script ",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><script><!--<script </script </script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script><!--<script </script </script>"
+      }
+    },
+    {
+      "data": "<script><!--<script </script </script>",
+      "errors": [
+        "(1,8): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "<!--<script </script ",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><script><!--<script </script </script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script><!--<script </script </script>"
+      }
+    },
+    {
+      "data": "<script><!--<script -",
+      "errors": [
+        "(1,8): expected-doctype-but-got-start-tag",
+        "(1,21): eof-in-script-in-script",
+        "(1,21): expected-named-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "<!--<script -",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><script><!--<script -</script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script><!--<script -</script>"
+      }
+    },
+    {
+      "data": "<script><!--<script -a",
+      "errors": [
+        "(1,8): expected-doctype-but-got-start-tag",
+        "(1,22): eof-in-script-in-script",
+        "(1,22): expected-named-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "<!--<script -a",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><script><!--<script -a</script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script><!--<script -a</script>"
+      }
+    },
+    {
+      "data": "<script><!--<script --",
+      "errors": [
+        "(1,8): expected-doctype-but-got-start-tag",
+        "(1,22): eof-in-script-in-script",
+        "(1,22): expected-named-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "<!--<script --",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><script><!--<script --</script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script><!--<script --</script>"
+      }
+    },
+    {
+      "data": "<script><!--<script --a",
+      "errors": [
+        "(1,8): expected-doctype-but-got-start-tag",
+        "(1,23): eof-in-script-in-script",
+        "(1,23): expected-named-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "<!--<script --a",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><script><!--<script --a</script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script><!--<script --a</script>"
+      }
+    },
+    {
+      "data": "<script><!--<script -->",
+      "errors": [
+        "(1,8): expected-doctype-but-got-start-tag",
+        "(1,23): expected-named-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "<!--<script -->",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><script><!--<script --></script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script><!--<script --></script>"
+      }
+    },
+    {
+      "data": "<script><!--<script --><",
+      "errors": [
+        "(1,8): expected-doctype-but-got-start-tag",
+        "(1,24): expected-named-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "<!--<script --><",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><script><!--<script --><</script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script><!--<script --><</script>"
+      }
+    },
+    {
+      "data": "<script><!--<script --></",
+      "errors": [
+        "(1,8): expected-doctype-but-got-start-tag",
+        "(1,25): expected-named-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "<!--<script --></",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><script><!--<script --></</script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script><!--<script --></</script>"
+      }
+    },
+    {
+      "data": "<script><!--<script --></script",
+      "errors": [
+        "(1,8): expected-doctype-but-got-start-tag",
+        "(1,31): expected-named-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "<!--<script --></script",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><script><!--<script --></script</script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script><!--<script --></script</script>"
+      }
+    },
+    {
+      "data": "<script><!--<script --></script ",
+      "errors": [
+        "(1,8): expected-doctype-but-got-start-tag",
+        "(1,32): expected-attribute-name-but-got-eof",
+        "(1,32): expected-named-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "<!--<script -->",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><script><!--<script --></script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script><!--<script --></script>"
+      }
+    },
+    {
+      "data": "<script><!--<script --></script/",
+      "errors": [
+        "(1,8): expected-doctype-but-got-start-tag",
+        "(1,32): unexpected-EOF-after-solidus-in-tag",
+        "(1,32): expected-named-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "<!--<script -->",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><script><!--<script --></script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script><!--<script --></script>"
+      }
+    },
+    {
+      "data": "<script><!--<script --></script>",
+      "errors": [
+        "(1,8): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "<!--<script -->",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><script><!--<script --></script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script><!--<script --></script>"
+      }
+    },
+    {
+      "data": "<script><!--<script><\\/script>--></script>",
+      "errors": [
+        "(1,8): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "<!--<script><\\/script>-->",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><script><!--<script><\\/script>--></script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script><!--<script><\\/script>--></script>"
+      }
+    },
+    {
+      "data": "<script><!--<script></scr'+'ipt>--></script>",
+      "errors": [
+        "(1,8): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "<!--<script></scr'+'ipt>-->",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><script><!--<script></scr'+'ipt>--></script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script><!--<script></scr'+'ipt>--></script>"
+      }
+    },
+    {
+      "data": "<script><!--<script></script><script></script></script>",
+      "errors": [
+        "(1,8): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "<!--<script></script><script></script>",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><script><!--<script></script><script></script></script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script><!--<script></script><script></script></script>"
+      }
+    },
+    {
+      "data": "<script><!--<script></script><script></script>--><!--</script>",
+      "errors": [
+        "(1,8): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "<!--<script></script><script></script>--><!--",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><script><!--<script></script><script></script>--><!--</script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script><!--<script></script><script></script>--><!--</script>"
+      }
+    },
+    {
+      "data": "<script><!--<script></script><script></script>-- ></script>",
+      "errors": [
+        "(1,8): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "<!--<script></script><script></script>-- >",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><script><!--<script></script><script></script>-- ></script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script><!--<script></script><script></script>-- ></script>"
+      }
+    },
+    {
+      "data": "<script><!--<script></script><script></script>- -></script>",
+      "errors": [
+        "(1,8): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "<!--<script></script><script></script>- ->",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><script><!--<script></script><script></script>- -></script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script><!--<script></script><script></script>- -></script>"
+      }
+    },
+    {
+      "data": "<script><!--<script></script><script></script>- - ></script>",
+      "errors": [
+        "(1,8): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "<!--<script></script><script></script>- - >",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><script><!--<script></script><script></script>- - ></script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script><!--<script></script><script></script>- - ></script>"
+      }
+    },
+    {
+      "data": "<script><!--<script></script><script></script>-></script>",
+      "errors": [
+        "(1,8): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "<!--<script></script><script></script>->",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><script><!--<script></script><script></script>-></script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script><!--<script></script><script></script>-></script>"
+      }
+    },
+    {
+      "data": "<script><!--<script>--!></script>X",
+      "errors": [
+        "(1,8): expected-doctype-but-got-start-tag",
+        "(1,34): expected-named-closing-tag-but-got-eof",
+        "(1,34): unexpected-eof-in-text-mode"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "<!--<script>--!></script>X",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><script><!--<script>--!></script>X</script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script><!--<script>--!></script>X</script>"
+      }
+    },
+    {
+      "data": "<script><!--<scr'+'ipt></script>--></script>",
+      "errors": [
+        "(1,8): expected-doctype-but-got-start-tag",
+        "(1,44): unexpected-end-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "no_escape": true,
+          "escaped": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "<!--<scr'+'ipt>",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "-->",
+                    "escaped": true
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><script><!--<scr'+'ipt></script></head><body>--&gt;</body></html>",
+        "noQuirksBodyHtml": "<script><!--<scr'+'ipt></script>--&gt;"
+      }
+    },
+    {
+      "data": "<script><!--<script></scr'+'ipt></script>X",
+      "errors": [
+        "(1,8): expected-doctype-but-got-start-tag",
+        "(1,42): expected-named-closing-tag-but-got-eof",
+        "(1,42): unexpected-eof-in-text-mode"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "<!--<script></scr'+'ipt></script>X",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><script><!--<script></scr'+'ipt></script>X</script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script><!--<script></scr'+'ipt></script>X</script>"
+      }
+    },
+    {
+      "data": "<style><!--<style></style>--></style>",
+      "errors": [
+        "(1,7): expected-doctype-but-got-start-tag",
+        "(1,37): unexpected-end-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "style": true,
+            "body": true
+          },
+          "no_escape": true,
+          "escaped": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "style",
+                    "children": [
+                      {
+                        "text": "<!--<style>",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "-->",
+                    "escaped": true
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><style><!--<style></style></head><body>--&gt;</body></html>",
+        "noQuirksBodyHtml": "<style><!--<style></style>--&gt;"
+      }
+    },
+    {
+      "data": "<style><!--</style>X",
+      "errors": [
+        "(1,7): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "style": true,
+            "body": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "style",
+                    "children": [
+                      {
+                        "text": "<!--",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "X"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><style><!--</style></head><body>X</body></html>",
+        "noQuirksBodyHtml": "<style><!--</style>X"
+      }
+    },
+    {
+      "data": "<style><!--...</style>...--></style>",
+      "errors": [
+        "(1,7): expected-doctype-but-got-start-tag",
+        "(1,36): unexpected-end-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "style": true,
+            "body": true
+          },
+          "no_escape": true,
+          "escaped": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "style",
+                    "children": [
+                      {
+                        "text": "<!--...",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "...-->",
+                    "escaped": true
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><style><!--...</style></head><body>...--&gt;</body></html>",
+        "noQuirksBodyHtml": "<style><!--...</style>...--&gt;"
+      }
+    },
+    {
+      "data": "<style><!--<br><html xmlns:v=\"urn:schemas-microsoft-com:vml\"><!--[if !mso]><style></style>X",
+      "errors": [
+        "(1,7): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "style": true,
+            "body": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "style",
+                    "children": [
+                      {
+                        "text": "<!--<br><html xmlns:v=\"urn:schemas-microsoft-com:vml\"><!--[if !mso]><style>",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "X"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><style><!--<br><html xmlns:v=\"urn:schemas-microsoft-com:vml\"><!--[if !mso]><style></style></head><body>X</body></html>",
+        "noQuirksBodyHtml": "<style><!--<br><html xmlns:v=\"urn:schemas-microsoft-com:vml\"><!--[if !mso]><style></style>X"
+      }
+    },
+    {
+      "data": "<style><!--...<style><!--...--!></style>--></style>",
+      "errors": [
+        "(1,7): expected-doctype-but-got-start-tag",
+        "(1,51): unexpected-end-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "style": true,
+            "body": true
+          },
+          "no_escape": true,
+          "escaped": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "style",
+                    "children": [
+                      {
+                        "text": "<!--...<style><!--...--!>",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "-->",
+                    "escaped": true
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><style><!--...<style><!--...--!></style></head><body>--&gt;</body></html>",
+        "noQuirksBodyHtml": "<style><!--...<style><!--...--!></style>--&gt;"
+      }
+    },
+    {
+      "data": "<style><!--...</style><!-- --><style>@import ...</style>",
+      "errors": [
+        "(1,7): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "style": true,
+            "body": true
+          },
+          "no_escape": true,
+          "comment": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "style",
+                    "children": [
+                      {
+                        "text": "<!--...",
+                        "no_escape": true
+                      }
+                    ]
+                  },
+                  {
+                    "comment": " "
+                  },
+                  {
+                    "tag": "style",
+                    "children": [
+                      {
+                        "text": "@import ...",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><style><!--...</style><!-- --><style>@import ...</style></head><body></body></html>",
+        "noQuirksBodyHtml": "<style><!--...</style><!-- --><style>@import ...</style>"
+      }
+    },
+    {
+      "data": "<style>...<style><!--...</style><!-- --></style>",
+      "errors": [
+        "(1,7): expected-doctype-but-got-start-tag",
+        "(1,48): unexpected-end-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "style": true,
+            "body": true
+          },
+          "no_escape": true,
+          "comment": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "style",
+                    "children": [
+                      {
+                        "text": "...<style><!--...",
+                        "no_escape": true
+                      }
+                    ]
+                  },
+                  {
+                    "comment": " "
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><style>...<style><!--...</style><!-- --></head><body></body></html>",
+        "noQuirksBodyHtml": "<style>...<style><!--...</style><!-- -->"
+      }
+    },
+    {
+      "data": "<style>...<!--[if IE]><style>...</style>X",
+      "errors": [
+        "(1,7): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "style": true,
+            "body": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "style",
+                    "children": [
+                      {
+                        "text": "...<!--[if IE]><style>...",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "X"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><style>...<!--[if IE]><style>...</style></head><body>X</body></html>",
+        "noQuirksBodyHtml": "<style>...<!--[if IE]><style>...</style>X"
+      }
+    },
+    {
+      "data": "<title><!--<title></title>--></title>",
+      "errors": [
+        "(1,7): expected-doctype-but-got-start-tag",
+        "(1,37): unexpected-end-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "title": true,
+            "body": true
+          },
+          "escaped": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "title",
+                    "children": [
+                      {
+                        "text": "<!--<title>",
+                        "escaped": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "-->",
+                    "escaped": true
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><title>&lt;!--&lt;title&gt;</title></head><body>--&gt;</body></html>",
+        "noQuirksBodyHtml": "<title>&lt;!--&lt;title&gt;</title>--&gt;"
+      }
+    },
+    {
+      "data": "<title>&lt;/title></title>",
+      "errors": [
+        "(1,7): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "title": true,
+            "body": true
+          },
+          "escaped": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "title",
+                    "children": [
+                      {
+                        "text": "</title>",
+                        "escaped": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><title>&lt;/title&gt;</title></head><body></body></html>",
+        "noQuirksBodyHtml": "<title>&lt;/title&gt;</title>"
+      }
+    },
+    {
+      "data": "<title>foo/title><link></head><body>X",
+      "errors": [
+        "(1,7): expected-doctype-but-got-start-tag",
+        "(1,37): expected-named-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "title": true,
+            "body": true
+          },
+          "escaped": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "title",
+                    "children": [
+                      {
+                        "text": "foo/title><link></head><body>X",
+                        "escaped": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><title>foo/title&gt;&lt;link&gt;&lt;/head&gt;&lt;body&gt;X</title></head><body></body></html>",
+        "noQuirksBodyHtml": "<title>foo/title&gt;&lt;link&gt;&lt;/head&gt;&lt;body&gt;X</title>"
+      }
+    },
+    {
+      "data": "<noscript><!--<noscript></noscript>--></noscript>",
+      "errors": [
+        "(1,10): expected-doctype-but-got-start-tag",
+        "(1,49): unexpected-end-tag"
+      ],
+      "script": "on",
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "noscript": true,
+            "body": true
+          },
+          "no_escape": true,
+          "escaped": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "noscript",
+                    "children": [
+                      {
+                        "text": "<!--<noscript>",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "-->",
+                    "escaped": true
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><noscript><!--<noscript></noscript></head><body>--&gt;</body></html>",
+        "noQuirksBodyHtml": "<noscript><!--<noscript></noscript>--></noscript>"
+      }
+    },
+    {
+      "data": "<noscript><!--<noscript></noscript>--></noscript>",
+      "errors": [
+        " * (1,11) missing DOCTYPE"
+      ],
+      "script": "off",
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "noscript": true,
+            "body": true
+          },
+          "comment": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "noscript",
+                    "children": [
+                      {
+                        "comment": "<noscript></noscript>"
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><noscript><!--<noscript></noscript>--></noscript></head><body></body></html>",
+        "noQuirksBodyHtml": "<noscript><!--<noscript></noscript>--></noscript>"
+      }
+    },
+    {
+      "data": "<noscript><!--</noscript>X<noscript>--></noscript>",
+      "errors": [
+        "(1,10): expected-doctype-but-got-start-tag"
+      ],
+      "script": "on",
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "noscript": true,
+            "body": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "noscript",
+                    "children": [
+                      {
+                        "text": "<!--",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "X"
+                  },
+                  {
+                    "tag": "noscript",
+                    "children": [
+                      {
+                        "text": "-->",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><noscript><!--</noscript></head><body>X<noscript>--></noscript></body></html>",
+        "noQuirksBodyHtml": "<noscript><!--</noscript>X<noscript>--></noscript>"
+      }
+    },
+    {
+      "data": "<noscript><!--</noscript>X<noscript>--></noscript>",
+      "errors": [
+        "(1,10): expected-doctype-but-got-start-tag"
+      ],
+      "script": "off",
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "noscript": true,
+            "body": true
+          },
+          "comment": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "noscript",
+                    "children": [
+                      {
+                        "comment": "</noscript>X<noscript>"
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><noscript><!--</noscript>X<noscript>--></noscript></head><body></body></html>",
+        "noQuirksBodyHtml": "<noscript><!--</noscript>X<noscript>--></noscript>"
+      }
+    },
+    {
+      "data": "<noscript><iframe></noscript>X",
+      "errors": [
+        "(1,10): expected-doctype-but-got-start-tag"
+      ],
+      "script": "on",
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "noscript": true,
+            "body": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "noscript",
+                    "children": [
+                      {
+                        "text": "<iframe>",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "X"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><noscript><iframe></noscript></head><body>X</body></html>",
+        "noQuirksBodyHtml": "<noscript><iframe></noscript>X</iframe></noscript>"
+      }
+    },
+    {
+      "data": "<noscript><iframe></noscript>X",
+      "errors": [
+        " * (1,11) missing DOCTYPE",
+        " * (1,19) unexpected token in head noscript",
+        " * (1,31) unexpected EOF"
+      ],
+      "script": "off",
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "noscript": true,
+            "body": true,
+            "iframe": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "noscript"
+                  }
+                ]
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "iframe",
+                    "children": [
+                      {
+                        "text": "</noscript>X",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><noscript></noscript></head><body><iframe></noscript>X</iframe></body></html>",
+        "noQuirksBodyHtml": "<noscript><iframe></noscript>X</iframe></noscript>"
+      }
+    },
+    {
+      "data": "<noframes><!--<noframes></noframes>--></noframes>",
+      "errors": [
+        "(1,10): expected-doctype-but-got-start-tag",
+        "(1,49): unexpected-end-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "noframes": true,
+            "body": true
+          },
+          "no_escape": true,
+          "escaped": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "noframes",
+                    "children": [
+                      {
+                        "text": "<!--<noframes>",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "-->",
+                    "escaped": true
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><noframes><!--<noframes></noframes></head><body>--&gt;</body></html>",
+        "noQuirksBodyHtml": "<noframes><!--<noframes></noframes>--&gt;"
+      }
+    },
+    {
+      "data": "<noframes><body><script><!--...</script></body></noframes></html>",
+      "errors": [
+        "(1,10): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "noframes": true,
+            "body": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "noframes",
+                    "children": [
+                      {
+                        "text": "<body><script><!--...</script></body>",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><noframes><body><script><!--...</script></body></noframes></head><body></body></html>",
+        "noQuirksBodyHtml": "<noframes><body><script><!--...</script></body></noframes>"
+      }
+    },
+    {
+      "data": "<textarea><!--<textarea></textarea>--></textarea>",
+      "errors": [
+        "(1,10): expected-doctype-but-got-start-tag",
+        "(1,49): unexpected-end-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "textarea": true
+          },
+          "escaped": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "textarea",
+                    "children": [
+                      {
+                        "text": "<!--<textarea>",
+                        "escaped": true
+                      }
+                    ]
+                  },
+                  {
+                    "text": "-->",
+                    "escaped": true
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><textarea>&lt;!--&lt;textarea&gt;</textarea>--&gt;</body></html>",
+        "noQuirksBodyHtml": "<textarea>&lt;!--&lt;textarea&gt;</textarea>--&gt;"
+      }
+    },
+    {
+      "data": "<textarea>&lt;/textarea></textarea>",
+      "errors": [
+        "(1,10): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "textarea": true
+          },
+          "escaped": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "textarea",
+                    "children": [
+                      {
+                        "text": "</textarea>",
+                        "escaped": true
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><textarea>&lt;/textarea&gt;</textarea></body></html>",
+        "noQuirksBodyHtml": "<textarea>&lt;/textarea&gt;</textarea>"
+      }
+    },
+    {
+      "data": "<iframe><!--<iframe></iframe>--></iframe>",
+      "errors": [
+        "(1,8): expected-doctype-but-got-start-tag",
+        "(1,41): unexpected-end-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "iframe": true
+          },
+          "no_escape": true,
+          "escaped": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "iframe",
+                    "children": [
+                      {
+                        "text": "<!--<iframe>",
+                        "no_escape": true
+                      }
+                    ]
+                  },
+                  {
+                    "text": "-->",
+                    "escaped": true
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><iframe><!--<iframe></iframe>--&gt;</body></html>",
+        "noQuirksBodyHtml": "<iframe><!--<iframe></iframe>--&gt;"
+      }
+    },
+    {
+      "data": "<iframe>...<!--X->...<!--/X->...</iframe>",
+      "errors": [
+        "(1,8): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "iframe": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "iframe",
+                    "children": [
+                      {
+                        "text": "...<!--X->...<!--/X->...",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><iframe>...<!--X->...<!--/X->...</iframe></body></html>",
+        "noQuirksBodyHtml": "<iframe>...<!--X->...<!--/X->...</iframe>"
+      }
+    },
+    {
+      "data": "<xmp><!--<xmp></xmp>--></xmp>",
+      "errors": [
+        "(1,5): expected-doctype-but-got-start-tag",
+        "(1,29): unexpected-end-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "xmp": true
+          },
+          "no_escape": true,
+          "escaped": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "xmp",
+                    "children": [
+                      {
+                        "text": "<!--<xmp>",
+                        "no_escape": true
+                      }
+                    ]
+                  },
+                  {
+                    "text": "-->",
+                    "escaped": true
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><xmp><!--<xmp></xmp>--&gt;</body></html>",
+        "noQuirksBodyHtml": "<xmp><!--<xmp></xmp>--&gt;"
+      }
+    },
+    {
+      "data": "<noembed><!--<noembed></noembed>--></noembed>",
+      "errors": [
+        "(1,9): expected-doctype-but-got-start-tag",
+        "(1,45): unexpected-end-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "noembed": true
+          },
+          "no_escape": true,
+          "escaped": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "noembed",
+                    "children": [
+                      {
+                        "text": "<!--<noembed>",
+                        "no_escape": true
+                      }
+                    ]
+                  },
+                  {
+                    "text": "-->",
+                    "escaped": true
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><noembed><!--<noembed></noembed>--&gt;</body></html>",
+        "noQuirksBodyHtml": "<noembed><!--<noembed></noembed>--&gt;"
+      }
+    },
+    {
+      "data": "<!doctype html><table>\n",
+      "errors": [
+        "(2,0): eof-in-table"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "text": "\n"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><table>\n</table></body></html>",
+        "noQuirksBodyHtml": "<table>\n</table>"
+      }
+    },
+    {
+      "data": "<!doctype html><table><td><span><font></span><span>",
+      "errors": [
+        "(1,26): unexpected-cell-in-table-body",
+        "(1,45): unexpected-end-tag",
+        "(1,51): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true,
+            "tbody": true,
+            "tr": true,
+            "td": true,
+            "span": true,
+            "font": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "tbody",
+                        "children": [
+                          {
+                            "tag": "tr",
+                            "children": [
+                              {
+                                "tag": "td",
+                                "children": [
+                                  {
+                                    "tag": "span",
+                                    "children": [
+                                      {
+                                        "tag": "font"
+                                      }
+                                    ]
+                                  },
+                                  {
+                                    "tag": "font",
+                                    "children": [
+                                      {
+                                        "tag": "span"
+                                      }
+                                    ]
+                                  }
+                                ]
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><table><tbody><tr><td><span><font></font></span><font><span></span></font></td></tr></tbody></table></body></html>",
+        "noQuirksBodyHtml": "<table><tbody><tr><td><span><font></font></span><font><span></span></font></td></tr></tbody></table>"
+      }
+    },
+    {
+      "data": "<!doctype html><form><table></form><form></table></form>",
+      "errors": [
+        "(1,35): unexpected-end-tag-implies-table-voodoo",
+        "(1,35): unexpected-end-tag",
+        "(1,41): unexpected-form-in-table",
+        "(1,56): unexpected-end-tag",
+        "(1,56): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "form": true,
+            "table": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "form",
+                    "children": [
+                      {
+                        "tag": "table",
+                        "children": [
+                          {
+                            "tag": "form"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><form><table><form></form></table></form></body></html>",
+        "noQuirksBodyHtml": "<form><table><form></form></table></form>"
+      }
+    }
+  ],
+  "tests17.dat": [
+    {
+      "data": "<!doctype html><table><tbody><select><tr>",
+      "errors": [
+        "(1,37): unexpected-start-tag-implies-table-voodoo",
+        "(1,41): unexpected-table-element-start-tag-in-select-in-table",
+        "(1,41): eof-in-table"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "select": true,
+            "table": true,
+            "tbody": true,
+            "tr": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "select"
+                  },
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "tbody",
+                        "children": [
+                          {
+                            "tag": "tr"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><select></select><table><tbody><tr></tr></tbody></table></body></html>",
+        "noQuirksBodyHtml": "<select></select><table><tbody><tr></tr></tbody></table>"
+      }
+    },
+    {
+      "data": "<!doctype html><table><tr><select><td>",
+      "errors": [
+        "(1,34): unexpected-start-tag-implies-table-voodoo",
+        "(1,38): unexpected-table-element-start-tag-in-select-in-table",
+        "(1,38): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "select": true,
+            "table": true,
+            "tbody": true,
+            "tr": true,
+            "td": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "select"
+                  },
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "tbody",
+                        "children": [
+                          {
+                            "tag": "tr",
+                            "children": [
+                              {
+                                "tag": "td"
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><select></select><table><tbody><tr><td></td></tr></tbody></table></body></html>",
+        "noQuirksBodyHtml": "<select></select><table><tbody><tr><td></td></tr></tbody></table>"
+      }
+    },
+    {
+      "data": "<!doctype html><table><tr><td><select><td>",
+      "errors": [
+        "(1,42): unexpected-table-element-start-tag-in-select-in-table",
+        "(1,42): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true,
+            "tbody": true,
+            "tr": true,
+            "td": true,
+            "select": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "tbody",
+                        "children": [
+                          {
+                            "tag": "tr",
+                            "children": [
+                              {
+                                "tag": "td",
+                                "children": [
+                                  {
+                                    "tag": "select"
+                                  }
+                                ]
+                              },
+                              {
+                                "tag": "td"
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><table><tbody><tr><td><select></select></td><td></td></tr></tbody></table></body></html>",
+        "noQuirksBodyHtml": "<table><tbody><tr><td><select></select></td><td></td></tr></tbody></table>"
+      }
+    },
+    {
+      "data": "<!doctype html><table><tr><th><select><td>",
+      "errors": [
+        "(1,42): unexpected-table-element-start-tag-in-select-in-table",
+        "(1,42): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true,
+            "tbody": true,
+            "tr": true,
+            "th": true,
+            "select": true,
+            "td": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "tbody",
+                        "children": [
+                          {
+                            "tag": "tr",
+                            "children": [
+                              {
+                                "tag": "th",
+                                "children": [
+                                  {
+                                    "tag": "select"
+                                  }
+                                ]
+                              },
+                              {
+                                "tag": "td"
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><table><tbody><tr><th><select></select></th><td></td></tr></tbody></table></body></html>",
+        "noQuirksBodyHtml": "<table><tbody><tr><th><select></select></th><td></td></tr></tbody></table>"
+      }
+    },
+    {
+      "data": "<!doctype html><table><caption><select><tr>",
+      "errors": [
+        "(1,43): unexpected-table-element-start-tag-in-select-in-table",
+        "(1,43): eof-in-table"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true,
+            "caption": true,
+            "select": true,
+            "tbody": true,
+            "tr": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "caption",
+                        "children": [
+                          {
+                            "tag": "select"
+                          }
+                        ]
+                      },
+                      {
+                        "tag": "tbody",
+                        "children": [
+                          {
+                            "tag": "tr"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><table><caption><select></select></caption><tbody><tr></tr></tbody></table></body></html>",
+        "noQuirksBodyHtml": "<table><caption><select></select></caption><tbody><tr></tr></tbody></table>"
+      }
+    },
+    {
+      "data": "<!doctype html><select><tr>",
+      "errors": [
+        "(1,27): unexpected-start-tag-in-select",
+        "(1,27): eof-in-select"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "select": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "select"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><select></select></body></html>",
+        "noQuirksBodyHtml": "<select></select>"
+      }
+    },
+    {
+      "data": "<!doctype html><select><td>",
+      "errors": [
+        "(1,27): unexpected-start-tag-in-select",
+        "(1,27): eof-in-select"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "select": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "select"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><select></select></body></html>",
+        "noQuirksBodyHtml": "<select></select>"
+      }
+    },
+    {
+      "data": "<!doctype html><select><th>",
+      "errors": [
+        "(1,27): unexpected-start-tag-in-select",
+        "(1,27): eof-in-select"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "select": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "select"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><select></select></body></html>",
+        "noQuirksBodyHtml": "<select></select>"
+      }
+    },
+    {
+      "data": "<!doctype html><select><tbody>",
+      "errors": [
+        "(1,30): unexpected-start-tag-in-select",
+        "(1,30): eof-in-select"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "select": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "select"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><select></select></body></html>",
+        "noQuirksBodyHtml": "<select></select>"
+      }
+    },
+    {
+      "data": "<!doctype html><select><thead>",
+      "errors": [
+        "(1,30): unexpected-start-tag-in-select",
+        "(1,30): eof-in-select"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "select": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "select"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><select></select></body></html>",
+        "noQuirksBodyHtml": "<select></select>"
+      }
+    },
+    {
+      "data": "<!doctype html><select><tfoot>",
+      "errors": [
+        "(1,30): unexpected-start-tag-in-select",
+        "(1,30): eof-in-select"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "select": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "select"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><select></select></body></html>",
+        "noQuirksBodyHtml": "<select></select>"
+      }
+    },
+    {
+      "data": "<!doctype html><select><caption>",
+      "errors": [
+        "(1,32): unexpected-start-tag-in-select",
+        "(1,32): eof-in-select"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "select": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "select"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><select></select></body></html>",
+        "noQuirksBodyHtml": "<select></select>"
+      }
+    },
+    {
+      "data": "<!doctype html><table><tr></table>a",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true,
+            "tbody": true,
+            "tr": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "tbody",
+                        "children": [
+                          {
+                            "tag": "tr"
+                          }
+                        ]
+                      }
+                    ]
+                  },
+                  {
+                    "text": "a"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><table><tbody><tr></tr></tbody></table>a</body></html>",
+        "noQuirksBodyHtml": "<table><tbody><tr></tr></tbody></table>a"
+      }
+    }
+  ],
+  "tests18.dat": [
+    {
+      "data": "<!doctype html><plaintext></plaintext>",
+      "errors": [
+        "(1,38): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "plaintext": true
+          },
+          "doctype": true,
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "plaintext",
+                    "children": [
+                      {
+                        "text": "</plaintext>",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><plaintext></plaintext></plaintext></body></html>",
+        "noQuirksBodyHtml": "<plaintext></plaintext></plaintext>"
+      }
+    },
+    {
+      "data": "<!doctype html><table><plaintext></plaintext>",
+      "errors": [
+        "(1,33): foster-parenting-start-tag",
+        "(1,34): foster-parenting-character",
+        "(1,35): foster-parenting-character",
+        "(1,36): foster-parenting-character",
+        "(1,37): foster-parenting-character",
+        "(1,38): foster-parenting-character",
+        "(1,39): foster-parenting-character",
+        "(1,40): foster-parenting-character",
+        "(1,41): foster-parenting-character",
+        "(1,42): foster-parenting-character",
+        "(1,43): foster-parenting-character",
+        "(1,44): foster-parenting-character",
+        "(1,45): foster-parenting-character",
+        "(1,45): eof-in-table"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "plaintext": true,
+            "table": true
+          },
+          "doctype": true,
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "plaintext",
+                    "children": [
+                      {
+                        "text": "</plaintext>",
+                        "no_escape": true
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "table"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><plaintext></plaintext></plaintext><table></table></body></html>",
+        "noQuirksBodyHtml": "<plaintext></plaintext></plaintext><table></table>"
+      }
+    },
+    {
+      "data": "<!doctype html><table><tbody><plaintext></plaintext>",
+      "errors": [
+        "(1,40): foster-parenting-start-tag",
+        "(1,41): foster-parenting-character",
+        "(1,41): foster-parenting-character",
+        "(1,41): foster-parenting-character",
+        "(1,41): foster-parenting-character",
+        "(1,41): foster-parenting-character",
+        "(1,41): foster-parenting-character",
+        "(1,41): foster-parenting-character",
+        "(1,41): foster-parenting-character",
+        "(1,41): foster-parenting-character",
+        "(1,41): foster-parenting-character",
+        "(1,41): foster-parenting-character",
+        "(1,41): foster-parenting-character",
+        "(1,52): eof-in-table"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "plaintext": true,
+            "table": true,
+            "tbody": true
+          },
+          "doctype": true,
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "plaintext",
+                    "children": [
+                      {
+                        "text": "</plaintext>",
+                        "no_escape": true
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "tbody"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><plaintext></plaintext></plaintext><table><tbody></tbody></table></body></html>",
+        "noQuirksBodyHtml": "<plaintext></plaintext></plaintext><table><tbody></tbody></table>"
+      }
+    },
+    {
+      "data": "<!doctype html><table><tbody><tr><plaintext></plaintext>",
+      "errors": [
+        "(1,44): foster-parenting-start-tag",
+        "(1,45): foster-parenting-character",
+        "(1,46): foster-parenting-character",
+        "(1,47): foster-parenting-character",
+        "(1,48): foster-parenting-character",
+        "(1,49): foster-parenting-character",
+        "(1,50): foster-parenting-character",
+        "(1,51): foster-parenting-character",
+        "(1,52): foster-parenting-character",
+        "(1,53): foster-parenting-character",
+        "(1,54): foster-parenting-character",
+        "(1,55): foster-parenting-character",
+        "(1,56): foster-parenting-character",
+        "(1,56): eof-in-table"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "plaintext": true,
+            "table": true,
+            "tbody": true,
+            "tr": true
+          },
+          "doctype": true,
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "plaintext",
+                    "children": [
+                      {
+                        "text": "</plaintext>",
+                        "no_escape": true
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "tbody",
+                        "children": [
+                          {
+                            "tag": "tr"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><plaintext></plaintext></plaintext><table><tbody><tr></tr></tbody></table></body></html>",
+        "noQuirksBodyHtml": "<plaintext></plaintext></plaintext><table><tbody><tr></tr></tbody></table>"
+      }
+    },
+    {
+      "data": "<!doctype html><table><td><plaintext></plaintext>",
+      "errors": [
+        "(1,26): unexpected-cell-in-table-body",
+        "(1,49): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true,
+            "tbody": true,
+            "tr": true,
+            "td": true,
+            "plaintext": true
+          },
+          "doctype": true,
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "tbody",
+                        "children": [
+                          {
+                            "tag": "tr",
+                            "children": [
+                              {
+                                "tag": "td",
+                                "children": [
+                                  {
+                                    "tag": "plaintext",
+                                    "children": [
+                                      {
+                                        "text": "</plaintext>",
+                                        "no_escape": true
+                                      }
+                                    ]
+                                  }
+                                ]
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><table><tbody><tr><td><plaintext></plaintext></plaintext></td></tr></tbody></table></body></html>",
+        "noQuirksBodyHtml": "<table><tbody><tr><td><plaintext></plaintext></plaintext></td></tr></tbody></table>"
+      }
+    },
+    {
+      "data": "<!doctype html><table><caption><plaintext></plaintext>",
+      "errors": [
+        "(1,54): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true,
+            "caption": true,
+            "plaintext": true
+          },
+          "doctype": true,
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "caption",
+                        "children": [
+                          {
+                            "tag": "plaintext",
+                            "children": [
+                              {
+                                "text": "</plaintext>",
+                                "no_escape": true
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><table><caption><plaintext></plaintext></plaintext></caption></table></body></html>",
+        "noQuirksBodyHtml": "<table><caption><plaintext></plaintext></plaintext></caption></table>"
+      }
+    },
+    {
+      "data": "<!doctype html><table><tr><style></script></style>abc",
+      "errors": [
+        "(1,51): foster-parenting-character",
+        "(1,52): foster-parenting-character",
+        "(1,53): foster-parenting-character",
+        "(1,53): eof-in-table"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true,
+            "tbody": true,
+            "tr": true,
+            "style": true
+          },
+          "doctype": true,
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "abc"
+                  },
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "tbody",
+                        "children": [
+                          {
+                            "tag": "tr",
+                            "children": [
+                              {
+                                "tag": "style",
+                                "children": [
+                                  {
+                                    "text": "</script>",
+                                    "no_escape": true
+                                  }
+                                ]
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body>abc<table><tbody><tr><style></script></style></tr></tbody></table></body></html>",
+        "noQuirksBodyHtml": "abc<table><tbody><tr><style></script></style></tr></tbody></table>"
+      }
+    },
+    {
+      "data": "<!doctype html><table><tr><script></style></script>abc",
+      "errors": [
+        "(1,52): foster-parenting-character",
+        "(1,53): foster-parenting-character",
+        "(1,54): foster-parenting-character",
+        "(1,54): eof-in-table"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true,
+            "tbody": true,
+            "tr": true,
+            "script": true
+          },
+          "doctype": true,
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "abc"
+                  },
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "tbody",
+                        "children": [
+                          {
+                            "tag": "tr",
+                            "children": [
+                              {
+                                "tag": "script",
+                                "children": [
+                                  {
+                                    "text": "</style>",
+                                    "no_escape": true
+                                  }
+                                ]
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body>abc<table><tbody><tr><script></style></script></tr></tbody></table></body></html>",
+        "noQuirksBodyHtml": "abc<table><tbody><tr><script></style></script></tr></tbody></table>"
+      }
+    },
+    {
+      "data": "<!doctype html><table><caption><style></script></style>abc",
+      "errors": [
+        "(1,58): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true,
+            "caption": true,
+            "style": true
+          },
+          "doctype": true,
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "caption",
+                        "children": [
+                          {
+                            "tag": "style",
+                            "children": [
+                              {
+                                "text": "</script>",
+                                "no_escape": true
+                              }
+                            ]
+                          },
+                          {
+                            "text": "abc"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><table><caption><style></script></style>abc</caption></table></body></html>",
+        "noQuirksBodyHtml": "<table><caption><style></script></style>abc</caption></table>"
+      }
+    },
+    {
+      "data": "<!doctype html><table><td><style></script></style>abc",
+      "errors": [
+        "(1,26): unexpected-cell-in-table-body",
+        "(1,53): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true,
+            "tbody": true,
+            "tr": true,
+            "td": true,
+            "style": true
+          },
+          "doctype": true,
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "tbody",
+                        "children": [
+                          {
+                            "tag": "tr",
+                            "children": [
+                              {
+                                "tag": "td",
+                                "children": [
+                                  {
+                                    "tag": "style",
+                                    "children": [
+                                      {
+                                        "text": "</script>",
+                                        "no_escape": true
+                                      }
+                                    ]
+                                  },
+                                  {
+                                    "text": "abc"
+                                  }
+                                ]
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><table><tbody><tr><td><style></script></style>abc</td></tr></tbody></table></body></html>",
+        "noQuirksBodyHtml": "<table><tbody><tr><td><style></script></style>abc</td></tr></tbody></table>"
+      }
+    },
+    {
+      "data": "<!doctype html><select><script></style></script>abc",
+      "errors": [
+        "(1,51): eof-in-select"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "select": true,
+            "script": true
+          },
+          "doctype": true,
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "select",
+                    "children": [
+                      {
+                        "tag": "script",
+                        "children": [
+                          {
+                            "text": "</style>",
+                            "no_escape": true
+                          }
+                        ]
+                      },
+                      {
+                        "text": "abc"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><select><script></style></script>abc</select></body></html>",
+        "noQuirksBodyHtml": "<select><script></style></script>abc</select>"
+      }
+    },
+    {
+      "data": "<!doctype html><table><select><script></style></script>abc",
+      "errors": [
+        "(1,30): unexpected-start-tag-implies-table-voodoo",
+        "(1,58): eof-in-select"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "select": true,
+            "script": true,
+            "table": true
+          },
+          "doctype": true,
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "select",
+                    "children": [
+                      {
+                        "tag": "script",
+                        "children": [
+                          {
+                            "text": "</style>",
+                            "no_escape": true
+                          }
+                        ]
+                      },
+                      {
+                        "text": "abc"
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "table"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><select><script></style></script>abc</select><table></table></body></html>",
+        "noQuirksBodyHtml": "<select><script></style></script>abc</select><table></table>"
+      }
+    },
+    {
+      "data": "<!doctype html><table><tr><select><script></style></script>abc",
+      "errors": [
+        "(1,34): unexpected-start-tag-implies-table-voodoo",
+        "(1,62): eof-in-select"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "select": true,
+            "script": true,
+            "table": true,
+            "tbody": true,
+            "tr": true
+          },
+          "doctype": true,
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "select",
+                    "children": [
+                      {
+                        "tag": "script",
+                        "children": [
+                          {
+                            "text": "</style>",
+                            "no_escape": true
+                          }
+                        ]
+                      },
+                      {
+                        "text": "abc"
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "tbody",
+                        "children": [
+                          {
+                            "tag": "tr"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><select><script></style></script>abc</select><table><tbody><tr></tr></tbody></table></body></html>",
+        "noQuirksBodyHtml": "<select><script></style></script>abc</select><table><tbody><tr></tr></tbody></table>"
+      }
+    },
+    {
+      "data": "<!doctype html><frameset></frameset><noframes>abc",
+      "errors": [
+        "(1,49): expected-named-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "frameset": true,
+            "noframes": true
+          },
+          "doctype": true,
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "frameset"
+              },
+              {
+                "tag": "noframes",
+                "children": [
+                  {
+                    "text": "abc",
+                    "no_escape": true
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><frameset></frameset><noframes>abc</noframes></html>",
+        "noQuirksBodyHtml": "<noframes>abc</noframes>"
+      }
+    },
+    {
+      "data": "<!doctype html><frameset></frameset><noframes>abc</noframes><!--abc-->",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "frameset": true,
+            "noframes": true
+          },
+          "doctype": true,
+          "no_escape": true,
+          "comment": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "frameset"
+              },
+              {
+                "tag": "noframes",
+                "children": [
+                  {
+                    "text": "abc",
+                    "no_escape": true
+                  }
+                ]
+              },
+              {
+                "comment": "abc"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><frameset></frameset><noframes>abc</noframes><!--abc--></html>",
+        "noQuirksBodyHtml": "<noframes>abc</noframes><!--abc-->"
+      }
+    },
+    {
+      "data": "<!doctype html><frameset></frameset></html><noframes>abc",
+      "errors": [
+        "(1,56): expected-named-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "frameset": true,
+            "noframes": true
+          },
+          "doctype": true,
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "frameset"
+              },
+              {
+                "tag": "noframes",
+                "children": [
+                  {
+                    "text": "abc",
+                    "no_escape": true
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><frameset></frameset><noframes>abc</noframes></html>",
+        "noQuirksBodyHtml": "<noframes>abc</noframes>"
+      }
+    },
+    {
+      "data": "<!doctype html><frameset></frameset></html><noframes>abc</noframes><!--abc-->",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "frameset": true,
+            "noframes": true
+          },
+          "doctype": true,
+          "no_escape": true,
+          "comment": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "frameset"
+              },
+              {
+                "tag": "noframes",
+                "children": [
+                  {
+                    "text": "abc",
+                    "no_escape": true
+                  }
+                ]
+              }
+            ]
+          },
+          {
+            "comment": "abc"
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><frameset></frameset><noframes>abc</noframes></html><!--abc-->",
+        "noQuirksBodyHtml": "<noframes>abc</noframes><!--abc-->"
+      }
+    },
+    {
+      "data": "<!doctype html><table><tr></tbody><tfoot>",
+      "errors": [
+        "(1,41): eof-in-table"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true,
+            "tbody": true,
+            "tr": true,
+            "tfoot": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "tbody",
+                        "children": [
+                          {
+                            "tag": "tr"
+                          }
+                        ]
+                      },
+                      {
+                        "tag": "tfoot"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><table><tbody><tr></tr></tbody><tfoot></tfoot></table></body></html>",
+        "noQuirksBodyHtml": "<table><tbody><tr></tr></tbody><tfoot></tfoot></table>"
+      }
+    },
+    {
+      "data": "<!doctype html><table><td><svg></svg>abc<td>",
+      "errors": [
+        "(1,26): unexpected-cell-in-table-body",
+        "(1,44): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true,
+            "tbody": true,
+            "tr": true,
+            "td": true,
+            "svg svg": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "tbody",
+                        "children": [
+                          {
+                            "tag": "tr",
+                            "children": [
+                              {
+                                "tag": "td",
+                                "children": [
+                                  {
+                                    "tag": "svg",
+                                    "ns": "http://www.w3.org/2000/svg"
+                                  },
+                                  {
+                                    "text": "abc"
+                                  }
+                                ]
+                              },
+                              {
+                                "tag": "td"
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><table><tbody><tr><td><svg></svg>abc</td><td></td></tr></tbody></table></body></html>",
+        "noQuirksBodyHtml": "<table><tbody><tr><td><svg></svg>abc</td><td></td></tr></tbody></table>"
+      }
+    }
+  ],
+  "tests19.dat": [
+    {
+      "data": "<!doctype html><math><mn DefinitionUrl=\"foo\">",
+      "errors": [
+        "(1,45): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "math math": true,
+            "math mn": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "math",
+                    "ns": "http://www.w3.org/1998/Math/MathML",
+                    "children": [
+                      {
+                        "tag": "mn",
+                        "ns": "http://www.w3.org/1998/Math/MathML",
+                        "attrs": [
+                          {
+                            "name": "definitionURL",
+                            "value": "foo"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><math><mn definitionURL=\"foo\"></mn></math></body></html>",
+        "noQuirksBodyHtml": "<math><mn definitionURL=\"foo\"></mn></math>"
+      }
+    },
+    {
+      "data": "<!doctype html><html></p><!--foo-->",
+      "errors": [
+        "(1,25): end-tag-after-implied-root"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "doctype": true,
+          "comment": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "comment": "foo"
+              },
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><!--foo--><head></head><body></body></html>",
+        "noQuirksBodyHtml": "<p></p><!--foo-->"
+      }
+    },
+    {
+      "data": "<!doctype html><head></head></p><!--foo-->",
+      "errors": [
+        "(1,32): unexpected-end-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "doctype": true,
+          "comment": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "comment": "foo"
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><!--foo--><body></body></html>",
+        "noQuirksBodyHtml": "<p></p><!--foo-->"
+      }
+    },
+    {
+      "data": "<!doctype html><body><p><pre>",
+      "errors": [
+        "(1,29): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "p": true,
+            "pre": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "p"
+                  },
+                  {
+                    "tag": "pre"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><p></p><pre></pre></body></html>",
+        "noQuirksBodyHtml": "<p></p><pre></pre>"
+      }
+    },
+    {
+      "data": "<!doctype html><body><p><listing>",
+      "errors": [
+        "(1,33): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "p": true,
+            "listing": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "p"
+                  },
+                  {
+                    "tag": "listing"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><p></p><listing></listing></body></html>",
+        "noQuirksBodyHtml": "<p></p><listing></listing>"
+      }
+    },
+    {
+      "data": "<!doctype html><p><plaintext>",
+      "errors": [
+        "(1,29): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "p": true,
+            "plaintext": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "p"
+                  },
+                  {
+                    "tag": "plaintext"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><p></p><plaintext></plaintext></body></html>",
+        "noQuirksBodyHtml": "<p></p><plaintext></plaintext>"
+      }
+    },
+    {
+      "data": "<!doctype html><p><h1>",
+      "errors": [
+        "(1,22): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "p": true,
+            "h1": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "p"
+                  },
+                  {
+                    "tag": "h1"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><p></p><h1></h1></body></html>",
+        "noQuirksBodyHtml": "<p></p><h1></h1>"
+      }
+    },
+    {
+      "data": "<!doctype html><isindex type=\"hidden\">",
+      "errors": [
+        "(1,38): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "isindex": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "isindex",
+                    "attrs": [
+                      {
+                        "name": "type",
+                        "value": "hidden"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><isindex type=\"hidden\"></isindex></body></html>",
+        "noQuirksBodyHtml": "<isindex type=\"hidden\"></isindex>"
+      }
+    },
+    {
+      "data": "<!doctype html><ruby><p><rp>",
+      "errors": [
+        "(1,28): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "ruby": true,
+            "p": true,
+            "rp": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "ruby",
+                    "children": [
+                      {
+                        "tag": "p"
+                      },
+                      {
+                        "tag": "rp"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><ruby><p></p><rp></rp></ruby></body></html>",
+        "noQuirksBodyHtml": "<ruby><p></p><rp></rp></ruby>"
+      }
+    },
+    {
+      "data": "<!doctype html><ruby><div><span><rp>",
+      "errors": [
+        "(1,36): XXX-undefined-error",
+        "(1,36): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "ruby": true,
+            "div": true,
+            "span": true,
+            "rp": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "ruby",
+                    "children": [
+                      {
+                        "tag": "div",
+                        "children": [
+                          {
+                            "tag": "span",
+                            "children": [
+                              {
+                                "tag": "rp"
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><ruby><div><span><rp></rp></span></div></ruby></body></html>",
+        "noQuirksBodyHtml": "<ruby><div><span><rp></rp></span></div></ruby>"
+      }
+    },
+    {
+      "data": "<!doctype html><ruby><div><p><rp>",
+      "errors": [
+        "(1,33): XXX-undefined-error",
+        "(1,33): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "ruby": true,
+            "div": true,
+            "p": true,
+            "rp": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "ruby",
+                    "children": [
+                      {
+                        "tag": "div",
+                        "children": [
+                          {
+                            "tag": "p"
+                          },
+                          {
+                            "tag": "rp"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><ruby><div><p></p><rp></rp></div></ruby></body></html>",
+        "noQuirksBodyHtml": "<ruby><div><p></p><rp></rp></div></ruby>"
+      }
+    },
+    {
+      "data": "<!doctype html><ruby><p><rt>",
+      "errors": [
+        "(1,28): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "ruby": true,
+            "p": true,
+            "rt": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "ruby",
+                    "children": [
+                      {
+                        "tag": "p"
+                      },
+                      {
+                        "tag": "rt"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><ruby><p></p><rt></rt></ruby></body></html>",
+        "noQuirksBodyHtml": "<ruby><p></p><rt></rt></ruby>"
+      }
+    },
+    {
+      "data": "<!doctype html><ruby><div><span><rt>",
+      "errors": [
+        "(1,36): XXX-undefined-error",
+        "(1,36): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "ruby": true,
+            "div": true,
+            "span": true,
+            "rt": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "ruby",
+                    "children": [
+                      {
+                        "tag": "div",
+                        "children": [
+                          {
+                            "tag": "span",
+                            "children": [
+                              {
+                                "tag": "rt"
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><ruby><div><span><rt></rt></span></div></ruby></body></html>",
+        "noQuirksBodyHtml": "<ruby><div><span><rt></rt></span></div></ruby>"
+      }
+    },
+    {
+      "data": "<!doctype html><ruby><div><p><rt>",
+      "errors": [
+        "(1,33): XXX-undefined-error",
+        "(1,33): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "ruby": true,
+            "div": true,
+            "p": true,
+            "rt": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "ruby",
+                    "children": [
+                      {
+                        "tag": "div",
+                        "children": [
+                          {
+                            "tag": "p"
+                          },
+                          {
+                            "tag": "rt"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><ruby><div><p></p><rt></rt></div></ruby></body></html>",
+        "noQuirksBodyHtml": "<ruby><div><p></p><rt></rt></div></ruby>"
+      }
+    },
+    {
+      "data": "<html><ruby>a<rb>b<rt></ruby></html>",
+      "errors": [
+        "(1,6): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "ruby": true,
+            "rb": true,
+            "rt": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "ruby",
+                    "children": [
+                      {
+                        "text": "a"
+                      },
+                      {
+                        "tag": "rb",
+                        "children": [
+                          {
+                            "text": "b"
+                          }
+                        ]
+                      },
+                      {
+                        "tag": "rt"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><ruby>a<rb>b</rb><rt></rt></ruby></body></html>",
+        "noQuirksBodyHtml": "<ruby>a<rb>b</rb><rt></rt></ruby>"
+      }
+    },
+    {
+      "data": "<html><ruby>a<rp>b<rt></ruby></html>",
+      "errors": [
+        "(1,6): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "ruby": true,
+            "rp": true,
+            "rt": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "ruby",
+                    "children": [
+                      {
+                        "text": "a"
+                      },
+                      {
+                        "tag": "rp",
+                        "children": [
+                          {
+                            "text": "b"
+                          }
+                        ]
+                      },
+                      {
+                        "tag": "rt"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><ruby>a<rp>b</rp><rt></rt></ruby></body></html>",
+        "noQuirksBodyHtml": "<ruby>a<rp>b</rp><rt></rt></ruby>"
+      }
+    },
+    {
+      "data": "<html><ruby>a<rt>b<rt></ruby></html>",
+      "errors": [
+        "(1,6): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "ruby": true,
+            "rt": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "ruby",
+                    "children": [
+                      {
+                        "text": "a"
+                      },
+                      {
+                        "tag": "rt",
+                        "children": [
+                          {
+                            "text": "b"
+                          }
+                        ]
+                      },
+                      {
+                        "tag": "rt"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><ruby>a<rt>b</rt><rt></rt></ruby></body></html>",
+        "noQuirksBodyHtml": "<ruby>a<rt>b</rt><rt></rt></ruby>"
+      }
+    },
+    {
+      "data": "<html><ruby>a<rtc>b<rt>c<rb>d</ruby></html>",
+      "errors": [
+        "(1,6): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "ruby": true,
+            "rtc": true,
+            "rt": true,
+            "rb": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "ruby",
+                    "children": [
+                      {
+                        "text": "a"
+                      },
+                      {
+                        "tag": "rtc",
+                        "children": [
+                          {
+                            "text": "b"
+                          },
+                          {
+                            "tag": "rt",
+                            "children": [
+                              {
+                                "text": "c"
+                              }
+                            ]
+                          }
+                        ]
+                      },
+                      {
+                        "tag": "rb",
+                        "children": [
+                          {
+                            "text": "d"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><ruby>a<rtc>b<rt>c</rt></rtc><rb>d</rb></ruby></body></html>",
+        "noQuirksBodyHtml": "<ruby>a<rtc>b<rt>c</rt></rtc><rb>d</rb></ruby>"
+      }
+    },
+    {
+      "data": "<!doctype html><math/><foo>",
+      "errors": [
+        "(1,27): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "math math": true,
+            "foo": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "math",
+                    "ns": "http://www.w3.org/1998/Math/MathML"
+                  },
+                  {
+                    "tag": "foo"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><math></math><foo></foo></body></html>",
+        "noQuirksBodyHtml": "<math></math><foo></foo>"
+      }
+    },
+    {
+      "data": "<!doctype html><svg/><foo>",
+      "errors": [
+        "(1,26): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "svg svg": true,
+            "foo": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "svg",
+                    "ns": "http://www.w3.org/2000/svg"
+                  },
+                  {
+                    "tag": "foo"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><svg></svg><foo></foo></body></html>",
+        "noQuirksBodyHtml": "<svg></svg><foo></foo>"
+      }
+    },
+    {
+      "data": "<!doctype html><div></body><!--foo-->",
+      "errors": [
+        "(1,27): expected-one-end-tag-but-got-another"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "div": true
+          },
+          "doctype": true,
+          "comment": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "div"
+                  }
+                ]
+              },
+              {
+                "comment": "foo"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><div></div></body><!--foo--></html>",
+        "noQuirksBodyHtml": "<div><!--foo--></div>"
+      }
+    },
+    {
+      "data": "<!doctype html><h1><div><h3><span></h1>foo",
+      "errors": [
+        "(1,39): end-tag-too-early",
+        "(1,42): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "h1": true,
+            "div": true,
+            "h3": true,
+            "span": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "h1",
+                    "children": [
+                      {
+                        "tag": "div",
+                        "children": [
+                          {
+                            "tag": "h3",
+                            "children": [
+                              {
+                                "tag": "span"
+                              }
+                            ]
+                          },
+                          {
+                            "text": "foo"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><h1><div><h3><span></span></h3>foo</div></h1></body></html>",
+        "noQuirksBodyHtml": "<h1><div><h3><span></span></h3>foo</div></h1>"
+      }
+    },
+    {
+      "data": "<!doctype html><p></h3>foo",
+      "errors": [
+        "(1,23): end-tag-too-early"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "p": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "p",
+                    "children": [
+                      {
+                        "text": "foo"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><p>foo</p></body></html>",
+        "noQuirksBodyHtml": "<p>foo</p>"
+      }
+    },
+    {
+      "data": "<!doctype html><h3><li>abc</h2>foo",
+      "errors": [
+        "(1,31): end-tag-too-early"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "h3": true,
+            "li": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "h3",
+                    "children": [
+                      {
+                        "tag": "li",
+                        "children": [
+                          {
+                            "text": "abc"
+                          }
+                        ]
+                      }
+                    ]
+                  },
+                  {
+                    "text": "foo"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><h3><li>abc</li></h3>foo</body></html>",
+        "noQuirksBodyHtml": "<h3><li>abc</li></h3>foo"
+      }
+    },
+    {
+      "data": "<!doctype html><table>abc<!--foo-->",
+      "errors": [
+        "(1,23): foster-parenting-character",
+        "(1,24): foster-parenting-character",
+        "(1,25): foster-parenting-character",
+        "(1,35): eof-in-table"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true
+          },
+          "doctype": true,
+          "comment": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "abc"
+                  },
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "comment": "foo"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body>abc<table><!--foo--></table></body></html>",
+        "noQuirksBodyHtml": "abc<table><!--foo--></table>"
+      }
+    },
+    {
+      "data": "<!doctype html><table>  <!--foo-->",
+      "errors": [
+        "(1,34): eof-in-table"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true
+          },
+          "doctype": true,
+          "comment": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "text": "  "
+                      },
+                      {
+                        "comment": "foo"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><table>  <!--foo--></table></body></html>",
+        "noQuirksBodyHtml": "<table>  <!--foo--></table>"
+      }
+    },
+    {
+      "data": "<!doctype html><table> b <!--foo-->",
+      "errors": [
+        "(1,23): foster-parenting-character",
+        "(1,24): foster-parenting-character",
+        "(1,25): foster-parenting-character",
+        "(1,35): eof-in-table"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true
+          },
+          "doctype": true,
+          "comment": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": " b "
+                  },
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "comment": "foo"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body> b <table><!--foo--></table></body></html>",
+        "noQuirksBodyHtml": " b <table><!--foo--></table>"
+      }
+    },
+    {
+      "data": "<!doctype html><select><option><option>",
+      "errors": [
+        "(1,39): eof-in-select"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "select": true,
+            "option": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "select",
+                    "children": [
+                      {
+                        "tag": "option"
+                      },
+                      {
+                        "tag": "option"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><select><option></option><option></option></select></body></html>",
+        "noQuirksBodyHtml": "<select><option></option><option></option></select>"
+      }
+    },
+    {
+      "data": "<!doctype html><select><option></optgroup>",
+      "errors": [
+        "(1,42): unexpected-end-tag-in-select",
+        "(1,42): eof-in-select"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "select": true,
+            "option": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "select",
+                    "children": [
+                      {
+                        "tag": "option"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><select><option></option></select></body></html>",
+        "noQuirksBodyHtml": "<select><option></option></select>"
+      }
+    },
+    {
+      "data": "<!doctype html><select><option></optgroup>",
+      "errors": [
+        "(1,42): unexpected-end-tag-in-select",
+        "(1,42): eof-in-select"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "select": true,
+            "option": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "select",
+                    "children": [
+                      {
+                        "tag": "option"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><select><option></option></select></body></html>",
+        "noQuirksBodyHtml": "<select><option></option></select>"
+      }
+    },
+    {
+      "data": "<!doctype html><dd><optgroup><dd>",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "dd": true,
+            "optgroup": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "dd",
+                    "children": [
+                      {
+                        "tag": "optgroup"
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "dd"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><dd><optgroup></optgroup></dd><dd></dd></body></html>",
+        "noQuirksBodyHtml": "<dd><optgroup></optgroup></dd><dd></dd>"
+      }
+    },
+    {
+      "data": "<!doctype html><p><math><mi><p><h1>",
+      "errors": [
+        "(1,35): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "p": true,
+            "math math": true,
+            "math mi": true,
+            "h1": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "p",
+                    "children": [
+                      {
+                        "tag": "math",
+                        "ns": "http://www.w3.org/1998/Math/MathML",
+                        "children": [
+                          {
+                            "tag": "mi",
+                            "ns": "http://www.w3.org/1998/Math/MathML",
+                            "children": [
+                              {
+                                "tag": "p"
+                              },
+                              {
+                                "tag": "h1"
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><p><math><mi><p></p><h1></h1></mi></math></p></body></html>",
+        "noQuirksBodyHtml": "<p><math><mi><p></p><h1></h1></mi></math></p>"
+      }
+    },
+    {
+      "data": "<!doctype html><p><math><mo><p><h1>",
+      "errors": [
+        "(1,35): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "p": true,
+            "math math": true,
+            "math mo": true,
+            "h1": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "p",
+                    "children": [
+                      {
+                        "tag": "math",
+                        "ns": "http://www.w3.org/1998/Math/MathML",
+                        "children": [
+                          {
+                            "tag": "mo",
+                            "ns": "http://www.w3.org/1998/Math/MathML",
+                            "children": [
+                              {
+                                "tag": "p"
+                              },
+                              {
+                                "tag": "h1"
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><p><math><mo><p></p><h1></h1></mo></math></p></body></html>",
+        "noQuirksBodyHtml": "<p><math><mo><p></p><h1></h1></mo></math></p>"
+      }
+    },
+    {
+      "data": "<!doctype html><p><math><mn><p><h1>",
+      "errors": [
+        "(1,35): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "p": true,
+            "math math": true,
+            "math mn": true,
+            "h1": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "p",
+                    "children": [
+                      {
+                        "tag": "math",
+                        "ns": "http://www.w3.org/1998/Math/MathML",
+                        "children": [
+                          {
+                            "tag": "mn",
+                            "ns": "http://www.w3.org/1998/Math/MathML",
+                            "children": [
+                              {
+                                "tag": "p"
+                              },
+                              {
+                                "tag": "h1"
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><p><math><mn><p></p><h1></h1></mn></math></p></body></html>",
+        "noQuirksBodyHtml": "<p><math><mn><p></p><h1></h1></mn></math></p>"
+      }
+    },
+    {
+      "data": "<!doctype html><p><math><ms><p><h1>",
+      "errors": [
+        "(1,35): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "p": true,
+            "math math": true,
+            "math ms": true,
+            "h1": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "p",
+                    "children": [
+                      {
+                        "tag": "math",
+                        "ns": "http://www.w3.org/1998/Math/MathML",
+                        "children": [
+                          {
+                            "tag": "ms",
+                            "ns": "http://www.w3.org/1998/Math/MathML",
+                            "children": [
+                              {
+                                "tag": "p"
+                              },
+                              {
+                                "tag": "h1"
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><p><math><ms><p></p><h1></h1></ms></math></p></body></html>",
+        "noQuirksBodyHtml": "<p><math><ms><p></p><h1></h1></ms></math></p>"
+      }
+    },
+    {
+      "data": "<!doctype html><p><math><mtext><p><h1>",
+      "errors": [
+        "(1,38): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "p": true,
+            "math math": true,
+            "math mtext": true,
+            "h1": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "p",
+                    "children": [
+                      {
+                        "tag": "math",
+                        "ns": "http://www.w3.org/1998/Math/MathML",
+                        "children": [
+                          {
+                            "tag": "mtext",
+                            "ns": "http://www.w3.org/1998/Math/MathML",
+                            "children": [
+                              {
+                                "tag": "p"
+                              },
+                              {
+                                "tag": "h1"
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><p><math><mtext><p></p><h1></h1></mtext></math></p></body></html>",
+        "noQuirksBodyHtml": "<p><math><mtext><p></p><h1></h1></mtext></math></p>"
+      }
+    },
+    {
+      "data": "<!doctype html><frameset></noframes>",
+      "errors": [
+        "(1,36): unexpected-end-tag-in-frameset",
+        "(1,36): eof-in-frameset"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "frameset": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "frameset"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><frameset></frameset></html>",
+        "noQuirksBodyHtml": ""
+      }
+    },
+    {
+      "data": "<!doctype html><html c=d><body></html><html a=b>",
+      "errors": [
+        "(1,48): non-html-root"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "attrs": [
+              {
+                "name": "a",
+                "value": "b"
+              },
+              {
+                "name": "c",
+                "value": "d"
+              }
+            ],
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html c=\"d\" a=\"b\"><head></head><body></body></html>",
+        "noQuirksBodyHtml": ""
+      }
+    },
+    {
+      "data": "<!doctype html><html c=d><frameset></frameset></html><html a=b>",
+      "errors": [
+        "(1,63): non-html-root"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "frameset": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "attrs": [
+              {
+                "name": "a",
+                "value": "b"
+              },
+              {
+                "name": "c",
+                "value": "d"
+              }
+            ],
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "frameset"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html c=\"d\" a=\"b\"><head></head><frameset></frameset></html>",
+        "noQuirksBodyHtml": ""
+      }
+    },
+    {
+      "data": "<!doctype html><html><frameset></frameset></html><!--foo-->",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "frameset": true
+          },
+          "doctype": true,
+          "comment": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "frameset"
+              }
+            ]
+          },
+          {
+            "comment": "foo"
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><frameset></frameset></html><!--foo-->",
+        "noQuirksBodyHtml": "<!--foo-->"
+      }
+    },
+    {
+      "data": "<!doctype html><html><frameset></frameset></html>  ",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "frameset": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "frameset"
+              },
+              {
+                "text": "  "
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><frameset></frameset>  </html>",
+        "noQuirksBodyHtml": "  "
+      }
+    },
+    {
+      "data": "<!doctype html><html><frameset></frameset></html>abc",
+      "errors": [
+        "(1,50): expected-eof-but-got-char",
+        "(1,51): expected-eof-but-got-char",
+        "(1,52): expected-eof-but-got-char"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "frameset": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "frameset"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><frameset></frameset></html>",
+        "noQuirksBodyHtml": "abc"
+      }
+    },
+    {
+      "data": "<!doctype html><html><frameset></frameset></html><p>",
+      "errors": [
+        "(1,52): expected-eof-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "frameset": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "frameset"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><frameset></frameset></html>",
+        "noQuirksBodyHtml": "<p></p>"
+      }
+    },
+    {
+      "data": "<!doctype html><html><frameset></frameset></html></p>",
+      "errors": [
+        "(1,53): expected-eof-but-got-end-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "frameset": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "frameset"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><frameset></frameset></html>",
+        "noQuirksBodyHtml": "<p></p>"
+      }
+    },
+    {
+      "data": "<html><frameset></frameset></html><!doctype html>",
+      "errors": [
+        "(1,6): expected-doctype-but-got-start-tag",
+        "(1,49): unexpected-doctype"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "frameset": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "frameset"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><frameset></frameset></html>",
+        "noQuirksBodyHtml": ""
+      }
+    },
+    {
+      "data": "<!doctype html><body><frameset>",
+      "errors": [
+        "(1,31): unexpected-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body></body></html>",
+        "noQuirksBodyHtml": ""
+      }
+    },
+    {
+      "data": "<!doctype html><p><frameset><frame>",
+      "errors": [
+        "(1,28): unexpected-start-tag",
+        "(1,35): eof-in-frameset"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "frameset": true,
+            "frame": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "frameset",
+                "children": [
+                  {
+                    "tag": "frame"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><frameset><frame></frameset></html>",
+        "noQuirksBodyHtml": "<p></p>"
+      }
+    },
+    {
+      "data": "<!doctype html><p>a<frameset>",
+      "errors": [
+        "(1,29): unexpected-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "p": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "p",
+                    "children": [
+                      {
+                        "text": "a"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><p>a</p></body></html>",
+        "noQuirksBodyHtml": "<p>a</p>"
+      }
+    },
+    {
+      "data": "<!doctype html><p> <frameset><frame>",
+      "errors": [
+        "(1,29): unexpected-start-tag",
+        "(1,36): eof-in-frameset"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "frameset": true,
+            "frame": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "frameset",
+                "children": [
+                  {
+                    "tag": "frame"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><frameset><frame></frameset></html>",
+        "noQuirksBodyHtml": "<p> </p>"
+      }
+    },
+    {
+      "data": "<!doctype html><pre><frameset>",
+      "errors": [
+        "(1,30): unexpected-start-tag",
+        "(1,30): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "pre": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "pre"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><pre></pre></body></html>",
+        "noQuirksBodyHtml": "<pre></pre>"
+      }
+    },
+    {
+      "data": "<!doctype html><listing><frameset>",
+      "errors": [
+        "(1,34): unexpected-start-tag",
+        "(1,34): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "listing": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "listing"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><listing></listing></body></html>",
+        "noQuirksBodyHtml": "<listing></listing>"
+      }
+    },
+    {
+      "data": "<!doctype html><li><frameset>",
+      "errors": [
+        "(1,29): unexpected-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "li": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "li"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><li></li></body></html>",
+        "noQuirksBodyHtml": "<li></li>"
+      }
+    },
+    {
+      "data": "<!doctype html><dd><frameset>",
+      "errors": [
+        "(1,29): unexpected-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "dd": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "dd"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><dd></dd></body></html>",
+        "noQuirksBodyHtml": "<dd></dd>"
+      }
+    },
+    {
+      "data": "<!doctype html><dt><frameset>",
+      "errors": [
+        "(1,29): unexpected-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "dt": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "dt"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><dt></dt></body></html>",
+        "noQuirksBodyHtml": "<dt></dt>"
+      }
+    },
+    {
+      "data": "<!doctype html><button><frameset>",
+      "errors": [
+        "(1,33): unexpected-start-tag",
+        "(1,33): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "button": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "button"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><button></button></body></html>",
+        "noQuirksBodyHtml": "<button></button>"
+      }
+    },
+    {
+      "data": "<!doctype html><applet><frameset>",
+      "errors": [
+        "(1,33): unexpected-start-tag",
+        "(1,33): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "applet": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "applet"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><applet></applet></body></html>",
+        "noQuirksBodyHtml": "<applet></applet>"
+      }
+    },
+    {
+      "data": "<!doctype html><marquee><frameset>",
+      "errors": [
+        "(1,34): unexpected-start-tag",
+        "(1,34): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "marquee": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "marquee"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><marquee></marquee></body></html>",
+        "noQuirksBodyHtml": "<marquee></marquee>"
+      }
+    },
+    {
+      "data": "<!doctype html><object><frameset>",
+      "errors": [
+        "(1,33): unexpected-start-tag",
+        "(1,33): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "object": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "object"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><object></object></body></html>",
+        "noQuirksBodyHtml": "<object></object>"
+      }
+    },
+    {
+      "data": "<!doctype html><table><frameset>",
+      "errors": [
+        "(1,32): unexpected-start-tag-implies-table-voodoo",
+        "(1,32): unexpected-start-tag",
+        "(1,32): eof-in-table"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "table"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><table></table></body></html>",
+        "noQuirksBodyHtml": "<table></table>"
+      }
+    },
+    {
+      "data": "<!doctype html><area><frameset>",
+      "errors": [
+        "(1,31): unexpected-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "area": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "area"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><area></body></html>",
+        "noQuirksBodyHtml": "<area>"
+      }
+    },
+    {
+      "data": "<!doctype html><basefont><frameset>",
+      "errors": [
+        "(1,35): eof-in-frameset"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "basefont": true,
+            "frameset": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "basefont"
+                  }
+                ]
+              },
+              {
+                "tag": "frameset"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head><basefont></head><frameset></frameset></html>",
+        "noQuirksBodyHtml": "<basefont>"
+      }
+    },
+    {
+      "data": "<!doctype html><bgsound><frameset>",
+      "errors": [
+        "(1,34): eof-in-frameset"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "bgsound": true,
+            "frameset": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "bgsound"
+                  }
+                ]
+              },
+              {
+                "tag": "frameset"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head><bgsound></head><frameset></frameset></html>",
+        "noQuirksBodyHtml": "<bgsound>"
+      }
+    },
+    {
+      "data": "<!doctype html><br><frameset>",
+      "errors": [
+        "(1,29): unexpected-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "br": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "br"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><br></body></html>",
+        "noQuirksBodyHtml": "<br>"
+      }
+    },
+    {
+      "data": "<!doctype html><embed><frameset>",
+      "errors": [
+        "(1,32): unexpected-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "embed": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "embed"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><embed></body></html>",
+        "noQuirksBodyHtml": "<embed>"
+      }
+    },
+    {
+      "data": "<!doctype html><img><frameset>",
+      "errors": [
+        "(1,30): unexpected-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "img": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "img"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><img></body></html>",
+        "noQuirksBodyHtml": "<img>"
+      }
+    },
+    {
+      "data": "<!doctype html><input><frameset>",
+      "errors": [
+        "(1,32): unexpected-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "input": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "input"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><input></body></html>",
+        "noQuirksBodyHtml": "<input>"
+      }
+    },
+    {
+      "data": "<!doctype html><keygen><frameset>",
+      "errors": [
+        "(1,33): unexpected-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "keygen": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "keygen"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><keygen></body></html>",
+        "noQuirksBodyHtml": "<keygen>"
+      }
+    },
+    {
+      "data": "<!doctype html><wbr><frameset>",
+      "errors": [
+        "(1,30): unexpected-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "wbr": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "wbr"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><wbr></body></html>",
+        "noQuirksBodyHtml": "<wbr>"
+      }
+    },
+    {
+      "data": "<!doctype html><hr><frameset>",
+      "errors": [
+        "(1,29): unexpected-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "hr": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "hr"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><hr></body></html>",
+        "noQuirksBodyHtml": "<hr>"
+      }
+    },
+    {
+      "data": "<!doctype html><textarea></textarea><frameset>",
+      "errors": [
+        "(1,46): unexpected-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "textarea": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "textarea"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><textarea></textarea></body></html>",
+        "noQuirksBodyHtml": "<textarea></textarea>"
+      }
+    },
+    {
+      "data": "<!doctype html><xmp></xmp><frameset>",
+      "errors": [
+        "(1,36): unexpected-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "xmp": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "xmp"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><xmp></xmp></body></html>",
+        "noQuirksBodyHtml": "<xmp></xmp>"
+      }
+    },
+    {
+      "data": "<!doctype html><iframe></iframe><frameset>",
+      "errors": [
+        "(1,42): unexpected-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "iframe": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "iframe"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><iframe></iframe></body></html>",
+        "noQuirksBodyHtml": "<iframe></iframe>"
+      }
+    },
+    {
+      "data": "<!doctype html><select></select><frameset>",
+      "errors": [
+        "(1,42): unexpected-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "select": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "select"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><select></select></body></html>",
+        "noQuirksBodyHtml": "<select></select>"
+      }
+    },
+    {
+      "data": "<!doctype html><svg></svg><frameset><frame>",
+      "errors": [
+        "(1,36): unexpected-start-tag",
+        "(1,43): eof-in-frameset"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "frameset": true,
+            "frame": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "frameset",
+                "children": [
+                  {
+                    "tag": "frame"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><frameset><frame></frameset></html>",
+        "noQuirksBodyHtml": "<svg></svg>"
+      }
+    },
+    {
+      "data": "<!doctype html><math></math><frameset><frame>",
+      "errors": [
+        "(1,38): unexpected-start-tag",
+        "(1,45): eof-in-frameset"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "frameset": true,
+            "frame": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "frameset",
+                "children": [
+                  {
+                    "tag": "frame"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><frameset><frame></frameset></html>",
+        "noQuirksBodyHtml": "<math></math>"
+      }
+    },
+    {
+      "data": "<!doctype html><svg><foreignObject><div> <frameset><frame>",
+      "errors": [
+        "(1,51): unexpected-start-tag",
+        "(1,58): eof-in-frameset"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "frameset": true,
+            "frame": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "frameset",
+                "children": [
+                  {
+                    "tag": "frame"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><frameset><frame></frameset></html>",
+        "noQuirksBodyHtml": "<svg><foreignObject><div> </div></foreignObject></svg>"
+      }
+    },
+    {
+      "data": "<!doctype html><svg>a</svg><frameset><frame>",
+      "errors": [
+        "(1,37): unexpected-start-tag",
+        "(1,44): unexpected-start-tag-ignored"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "svg svg": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "svg",
+                    "ns": "http://www.w3.org/2000/svg",
+                    "children": [
+                      {
+                        "text": "a"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><svg>a</svg></body></html>",
+        "noQuirksBodyHtml": "<svg>a</svg>"
+      }
+    },
+    {
+      "data": "<!doctype html><svg> </svg><frameset><frame>",
+      "errors": [
+        "(1,37): unexpected-start-tag",
+        "(1,44): eof-in-frameset"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "frameset": true,
+            "frame": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "frameset",
+                "children": [
+                  {
+                    "tag": "frame"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><frameset><frame></frameset></html>",
+        "noQuirksBodyHtml": "<svg> </svg>"
+      }
+    },
+    {
+      "data": "<html>aaa<frameset></frameset>",
+      "errors": [
+        "(1,6): expected-doctype-but-got-start-tag",
+        "(1,19): unexpected-start-tag",
+        "(1,30): unexpected-end-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "aaa"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>aaa</body></html>",
+        "noQuirksBodyHtml": "aaa"
+      }
+    },
+    {
+      "data": "<html> a <frameset></frameset>",
+      "errors": [
+        "(1,6): expected-doctype-but-got-start-tag",
+        "(1,19): unexpected-start-tag",
+        "(1,30): unexpected-end-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "a "
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>a </body></html>",
+        "noQuirksBodyHtml": " a "
+      }
+    },
+    {
+      "data": "<!doctype html><div><frameset>",
+      "errors": [
+        "(1,30): unexpected-start-tag",
+        "(1,30): eof-in-frameset"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "frameset": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "frameset"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><frameset></frameset></html>",
+        "noQuirksBodyHtml": "<div></div>"
+      }
+    },
+    {
+      "data": "<!doctype html><div><body><frameset>",
+      "errors": [
+        "(1,26): unexpected-start-tag",
+        "(1,36): unexpected-start-tag",
+        "(1,36): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "div": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "div"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><div></div></body></html>",
+        "noQuirksBodyHtml": "<div></div>"
+      }
+    },
+    {
+      "data": "<!doctype html><p><math></p>a",
+      "errors": [
+        "(1,28): unexpected-end-tag",
+        "(1,28): unexpected-end-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "p": true,
+            "math math": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "p",
+                    "children": [
+                      {
+                        "tag": "math",
+                        "ns": "http://www.w3.org/1998/Math/MathML"
+                      }
+                    ]
+                  },
+                  {
+                    "text": "a"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><p><math></math></p>a</body></html>",
+        "noQuirksBodyHtml": "<p><math></math></p>a"
+      }
+    },
+    {
+      "data": "<!doctype html><p><math><mn><span></p>a",
+      "errors": [
+        "(1,38): unexpected-end-tag",
+        "(1,39): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "p": true,
+            "math math": true,
+            "math mn": true,
+            "span": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "p",
+                    "children": [
+                      {
+                        "tag": "math",
+                        "ns": "http://www.w3.org/1998/Math/MathML",
+                        "children": [
+                          {
+                            "tag": "mn",
+                            "ns": "http://www.w3.org/1998/Math/MathML",
+                            "children": [
+                              {
+                                "tag": "span",
+                                "children": [
+                                  {
+                                    "tag": "p"
+                                  },
+                                  {
+                                    "text": "a"
+                                  }
+                                ]
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><p><math><mn><span><p></p>a</span></mn></math></p></body></html>",
+        "noQuirksBodyHtml": "<p><math><mn><span><p></p>a</span></mn></math></p>"
+      }
+    },
+    {
+      "data": "<!doctype html><math></html>",
+      "errors": [
+        "(1,28): unexpected-end-tag",
+        "(1,28): expected-one-end-tag-but-got-another",
+        "(1,28): unexpected-end-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "math math": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "math",
+                    "ns": "http://www.w3.org/1998/Math/MathML"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><math></math></body></html>",
+        "noQuirksBodyHtml": "<math></math>"
+      }
+    },
+    {
+      "data": "<!doctype html><meta charset=\"ascii\">",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "meta": true,
+            "body": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "meta",
+                    "attrs": [
+                      {
+                        "name": "charset",
+                        "value": "ascii"
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head><meta charset=\"ascii\"></head><body></body></html>",
+        "noQuirksBodyHtml": "<meta charset=\"ascii\">"
+      }
+    },
+    {
+      "data": "<!doctype html><meta http-equiv=\"content-type\" content=\"text/html;charset=ascii\">",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "meta": true,
+            "body": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "meta",
+                    "attrs": [
+                      {
+                        "name": "content",
+                        "value": "text/html;charset=ascii"
+                      },
+                      {
+                        "name": "http-equiv",
+                        "value": "content-type"
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head><meta http-equiv=\"content-type\" content=\"text/html;charset=ascii\"></head><body></body></html>",
+        "noQuirksBodyHtml": "<meta http-equiv=\"content-type\" content=\"text/html;charset=ascii\">"
+      }
+    },
+    {
+      "data": "<!doctype html><head><!--aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa--><meta charset=\"utf8\">",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "meta": true,
+            "body": true
+          },
+          "doctype": true,
+          "comment": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "comment": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
+                  },
+                  {
+                    "tag": "meta",
+                    "attrs": [
+                      {
+                        "name": "charset",
+                        "value": "utf8"
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head><!--aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa--><meta charset=\"utf8\"></head><body></body></html>",
+        "noQuirksBodyHtml": "<!--aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa--><meta charset=\"utf8\">"
+      }
+    },
+    {
+      "data": "<!doctype html><html a=b><head></head><html c=d>",
+      "errors": [
+        "(1,48): non-html-root"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "attrs": [
+              {
+                "name": "a",
+                "value": "b"
+              },
+              {
+                "name": "c",
+                "value": "d"
+              }
+            ],
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html a=\"b\" c=\"d\"><head></head><body></body></html>",
+        "noQuirksBodyHtml": ""
+      }
+    },
+    {
+      "data": "<!doctype html><image/>",
+      "errors": [
+        "(1,23): image-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "img": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "img"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><img></body></html>",
+        "noQuirksBodyHtml": "<img>"
+      }
+    },
+    {
+      "data": "<!doctype html>a<i>b<table>c<b>d</i>e</b>f",
+      "errors": [
+        "(1,28): foster-parenting-character",
+        "(1,31): foster-parenting-start-tag",
+        "(1,32): foster-parenting-character",
+        "(1,36): foster-parenting-end-tag",
+        "(1,36): adoption-agency-1.3",
+        "(1,37): foster-parenting-character",
+        "(1,41): foster-parenting-end-tag",
+        "(1,42): foster-parenting-character",
+        "(1,42): eof-in-table"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "i": true,
+            "b": true,
+            "table": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "a"
+                  },
+                  {
+                    "tag": "i",
+                    "children": [
+                      {
+                        "text": "bc"
+                      },
+                      {
+                        "tag": "b",
+                        "children": [
+                          {
+                            "text": "de"
+                          }
+                        ]
+                      },
+                      {
+                        "text": "f"
+                      },
+                      {
+                        "tag": "table"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body>a<i>bc<b>de</b>f<table></table></i></body></html>",
+        "noQuirksBodyHtml": "a<i>bc<b>de</b>f<table></table></i>"
+      }
+    },
+    {
+      "data": "<!doctype html><table><i>a<b>b<div>c<a>d</i>e</b>f",
+      "errors": [
+        "(1,25): foster-parenting-start-tag",
+        "(1,26): foster-parenting-character",
+        "(1,29): foster-parenting-start-tag",
+        "(1,30): foster-parenting-character",
+        "(1,35): foster-parenting-start-tag",
+        "(1,36): foster-parenting-character",
+        "(1,39): foster-parenting-start-tag",
+        "(1,40): foster-parenting-character",
+        "(1,44): foster-parenting-end-tag",
+        "(1,44): adoption-agency-1.3",
+        "(1,44): adoption-agency-1.3",
+        "(1,45): foster-parenting-character",
+        "(1,49): foster-parenting-end-tag",
+        "(1,49): adoption-agency-1.3",
+        "(1,49): adoption-agency-1.3",
+        "(1,50): foster-parenting-character",
+        "(1,50): eof-in-table"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "i": true,
+            "b": true,
+            "div": true,
+            "a": true,
+            "table": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "i",
+                    "children": [
+                      {
+                        "text": "a"
+                      },
+                      {
+                        "tag": "b",
+                        "children": [
+                          {
+                            "text": "b"
+                          }
+                        ]
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "b"
+                  },
+                  {
+                    "tag": "div",
+                    "children": [
+                      {
+                        "tag": "b",
+                        "children": [
+                          {
+                            "tag": "i",
+                            "children": [
+                              {
+                                "text": "c"
+                              },
+                              {
+                                "tag": "a",
+                                "children": [
+                                  {
+                                    "text": "d"
+                                  }
+                                ]
+                              }
+                            ]
+                          },
+                          {
+                            "tag": "a",
+                            "children": [
+                              {
+                                "text": "e"
+                              }
+                            ]
+                          }
+                        ]
+                      },
+                      {
+                        "tag": "a",
+                        "children": [
+                          {
+                            "text": "f"
+                          }
+                        ]
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "table"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><i>a<b>b</b></i><b></b><div><b><i>c<a>d</a></i><a>e</a></b><a>f</a></div><table></table></body></html>",
+        "noQuirksBodyHtml": "<i>a<b>b</b></i><b></b><div><b><i>c<a>d</a></i><a>e</a></b><a>f</a></div><table></table>"
+      }
+    },
+    {
+      "data": "<!doctype html><i>a<b>b<div>c<a>d</i>e</b>f",
+      "errors": [
+        "(1,37): adoption-agency-1.3",
+        "(1,37): adoption-agency-1.3",
+        "(1,42): adoption-agency-1.3",
+        "(1,42): adoption-agency-1.3",
+        "(1,43): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "i": true,
+            "b": true,
+            "div": true,
+            "a": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "i",
+                    "children": [
+                      {
+                        "text": "a"
+                      },
+                      {
+                        "tag": "b",
+                        "children": [
+                          {
+                            "text": "b"
+                          }
+                        ]
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "b"
+                  },
+                  {
+                    "tag": "div",
+                    "children": [
+                      {
+                        "tag": "b",
+                        "children": [
+                          {
+                            "tag": "i",
+                            "children": [
+                              {
+                                "text": "c"
+                              },
+                              {
+                                "tag": "a",
+                                "children": [
+                                  {
+                                    "text": "d"
+                                  }
+                                ]
+                              }
+                            ]
+                          },
+                          {
+                            "tag": "a",
+                            "children": [
+                              {
+                                "text": "e"
+                              }
+                            ]
+                          }
+                        ]
+                      },
+                      {
+                        "tag": "a",
+                        "children": [
+                          {
+                            "text": "f"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><i>a<b>b</b></i><b></b><div><b><i>c<a>d</a></i><a>e</a></b><a>f</a></div></body></html>",
+        "noQuirksBodyHtml": "<i>a<b>b</b></i><b></b><div><b><i>c<a>d</a></i><a>e</a></b><a>f</a></div>"
+      }
+    },
+    {
+      "data": "<!doctype html><table><i>a<b>b<div>c</i>",
+      "errors": [
+        "(1,25): foster-parenting-start-tag",
+        "(1,26): foster-parenting-character",
+        "(1,29): foster-parenting-start-tag",
+        "(1,30): foster-parenting-character",
+        "(1,35): foster-parenting-start-tag",
+        "(1,36): foster-parenting-character",
+        "(1,40): foster-parenting-end-tag",
+        "(1,40): adoption-agency-1.3",
+        "(1,40): eof-in-table"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "i": true,
+            "b": true,
+            "div": true,
+            "table": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "i",
+                    "children": [
+                      {
+                        "text": "a"
+                      },
+                      {
+                        "tag": "b",
+                        "children": [
+                          {
+                            "text": "b"
+                          }
+                        ]
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "b",
+                    "children": [
+                      {
+                        "tag": "div",
+                        "children": [
+                          {
+                            "tag": "i",
+                            "children": [
+                              {
+                                "text": "c"
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "table"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><i>a<b>b</b></i><b><div><i>c</i></div></b><table></table></body></html>",
+        "noQuirksBodyHtml": "<i>a<b>b</b></i><b><div><i>c</i></div></b><table></table>"
+      }
+    },
+    {
+      "data": "<!doctype html><table><i>a<b>b<div>c<a>d</i>e</b>f",
+      "errors": [
+        "(1,25): foster-parenting-start-tag",
+        "(1,26): foster-parenting-character",
+        "(1,29): foster-parenting-start-tag",
+        "(1,30): foster-parenting-character",
+        "(1,35): foster-parenting-start-tag",
+        "(1,36): foster-parenting-character",
+        "(1,39): foster-parenting-start-tag",
+        "(1,40): foster-parenting-character",
+        "(1,44): foster-parenting-end-tag",
+        "(1,44): adoption-agency-1.3",
+        "(1,44): adoption-agency-1.3",
+        "(1,45): foster-parenting-character",
+        "(1,49): foster-parenting-end-tag",
+        "(1,44): adoption-agency-1.3",
+        "(1,44): adoption-agency-1.3",
+        "(1,50): foster-parenting-character",
+        "(1,50): eof-in-table"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "i": true,
+            "b": true,
+            "div": true,
+            "a": true,
+            "table": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "i",
+                    "children": [
+                      {
+                        "text": "a"
+                      },
+                      {
+                        "tag": "b",
+                        "children": [
+                          {
+                            "text": "b"
+                          }
+                        ]
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "b"
+                  },
+                  {
+                    "tag": "div",
+                    "children": [
+                      {
+                        "tag": "b",
+                        "children": [
+                          {
+                            "tag": "i",
+                            "children": [
+                              {
+                                "text": "c"
+                              },
+                              {
+                                "tag": "a",
+                                "children": [
+                                  {
+                                    "text": "d"
+                                  }
+                                ]
+                              }
+                            ]
+                          },
+                          {
+                            "tag": "a",
+                            "children": [
+                              {
+                                "text": "e"
+                              }
+                            ]
+                          }
+                        ]
+                      },
+                      {
+                        "tag": "a",
+                        "children": [
+                          {
+                            "text": "f"
+                          }
+                        ]
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "table"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><i>a<b>b</b></i><b></b><div><b><i>c<a>d</a></i><a>e</a></b><a>f</a></div><table></table></body></html>",
+        "noQuirksBodyHtml": "<i>a<b>b</b></i><b></b><div><b><i>c<a>d</a></i><a>e</a></b><a>f</a></div><table></table>"
+      }
+    },
+    {
+      "data": "<!doctype html><table><i>a<div>b<tr>c<b>d</i>e",
+      "errors": [
+        "(1,25): foster-parenting-start-tag",
+        "(1,26): foster-parenting-character",
+        "(1,31): foster-parenting-start-tag",
+        "(1,32): foster-parenting-character",
+        "(1,37): foster-parenting-character",
+        "(1,40): foster-parenting-start-tag",
+        "(1,41): foster-parenting-character",
+        "(1,45): foster-parenting-end-tag",
+        "(1,45): adoption-agency-1.3",
+        "(1,46): foster-parenting-character",
+        "(1,46): eof-in-table"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "i": true,
+            "div": true,
+            "b": true,
+            "table": true,
+            "tbody": true,
+            "tr": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "i",
+                    "children": [
+                      {
+                        "text": "a"
+                      },
+                      {
+                        "tag": "div",
+                        "children": [
+                          {
+                            "text": "b"
+                          }
+                        ]
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "i",
+                    "children": [
+                      {
+                        "text": "c"
+                      },
+                      {
+                        "tag": "b",
+                        "children": [
+                          {
+                            "text": "d"
+                          }
+                        ]
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "b",
+                    "children": [
+                      {
+                        "text": "e"
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "tbody",
+                        "children": [
+                          {
+                            "tag": "tr"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><i>a<div>b</div></i><i>c<b>d</b></i><b>e</b><table><tbody><tr></tr></tbody></table></body></html>",
+        "noQuirksBodyHtml": "<i>a<div>b</div></i><i>c<b>d</b></i><b>e</b><table><tbody><tr></tr></tbody></table>"
+      }
+    },
+    {
+      "data": "<!doctype html><table><td><table><i>a<div>b<b>c</i>d",
+      "errors": [
+        "(1,26): unexpected-cell-in-table-body",
+        "(1,36): foster-parenting-start-tag",
+        "(1,37): foster-parenting-character",
+        "(1,42): foster-parenting-start-tag",
+        "(1,43): foster-parenting-character",
+        "(1,46): foster-parenting-start-tag",
+        "(1,47): foster-parenting-character",
+        "(1,51): foster-parenting-end-tag",
+        "(1,51): adoption-agency-1.3",
+        "(1,51): adoption-agency-1.3",
+        "(1,52): foster-parenting-character",
+        "(1,52): eof-in-table"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true,
+            "tbody": true,
+            "tr": true,
+            "td": true,
+            "i": true,
+            "div": true,
+            "b": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "tbody",
+                        "children": [
+                          {
+                            "tag": "tr",
+                            "children": [
+                              {
+                                "tag": "td",
+                                "children": [
+                                  {
+                                    "tag": "i",
+                                    "children": [
+                                      {
+                                        "text": "a"
+                                      }
+                                    ]
+                                  },
+                                  {
+                                    "tag": "div",
+                                    "children": [
+                                      {
+                                        "tag": "i",
+                                        "children": [
+                                          {
+                                            "text": "b"
+                                          },
+                                          {
+                                            "tag": "b",
+                                            "children": [
+                                              {
+                                                "text": "c"
+                                              }
+                                            ]
+                                          }
+                                        ]
+                                      },
+                                      {
+                                        "tag": "b",
+                                        "children": [
+                                          {
+                                            "text": "d"
+                                          }
+                                        ]
+                                      }
+                                    ]
+                                  },
+                                  {
+                                    "tag": "table"
+                                  }
+                                ]
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><table><tbody><tr><td><i>a</i><div><i>b<b>c</b></i><b>d</b></div><table></table></td></tr></tbody></table></body></html>",
+        "noQuirksBodyHtml": "<table><tbody><tr><td><i>a</i><div><i>b<b>c</b></i><b>d</b></div><table></table></td></tr></tbody></table>"
+      }
+    },
+    {
+      "data": "<!doctype html><body><bgsound>",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "bgsound": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "bgsound"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><bgsound></body></html>",
+        "noQuirksBodyHtml": "<bgsound>"
+      }
+    },
+    {
+      "data": "<!doctype html><body><basefont>",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "basefont": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "basefont"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><basefont></body></html>",
+        "noQuirksBodyHtml": "<basefont>"
+      }
+    },
+    {
+      "data": "<!doctype html><a><b></a><basefont>",
+      "errors": [
+        "(1,25): adoption-agency-1.3"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "a": true,
+            "b": true,
+            "basefont": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "a",
+                    "children": [
+                      {
+                        "tag": "b"
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "basefont"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><a><b></b></a><basefont></body></html>",
+        "noQuirksBodyHtml": "<a><b></b></a><basefont>"
+      }
+    },
+    {
+      "data": "<!doctype html><a><b></a><bgsound>",
+      "errors": [
+        "(1,25): adoption-agency-1.3"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "a": true,
+            "b": true,
+            "bgsound": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "a",
+                    "children": [
+                      {
+                        "tag": "b"
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "bgsound"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><a><b></b></a><bgsound></body></html>",
+        "noQuirksBodyHtml": "<a><b></b></a><bgsound>"
+      }
+    },
+    {
+      "data": "<!doctype html><figcaption><article></figcaption>a",
+      "errors": [
+        "(1,49): end-tag-too-early"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "figcaption": true,
+            "article": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "figcaption",
+                    "children": [
+                      {
+                        "tag": "article"
+                      }
+                    ]
+                  },
+                  {
+                    "text": "a"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><figcaption><article></article></figcaption>a</body></html>",
+        "noQuirksBodyHtml": "<figcaption><article></article></figcaption>a"
+      }
+    },
+    {
+      "data": "<!doctype html><summary><article></summary>a",
+      "errors": [
+        "(1,43): end-tag-too-early"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "summary": true,
+            "article": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "summary",
+                    "children": [
+                      {
+                        "tag": "article"
+                      }
+                    ]
+                  },
+                  {
+                    "text": "a"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><summary><article></article></summary>a</body></html>",
+        "noQuirksBodyHtml": "<summary><article></article></summary>a"
+      }
+    },
+    {
+      "data": "<!doctype html><p><a><plaintext>b",
+      "errors": [
+        "(1,32): unexpected-end-tag",
+        "(1,33): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "p": true,
+            "a": true,
+            "plaintext": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "p",
+                    "children": [
+                      {
+                        "tag": "a"
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "plaintext",
+                    "children": [
+                      {
+                        "tag": "a",
+                        "children": [
+                          {
+                            "text": "b"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><p><a></a></p><plaintext><a>b</a></plaintext></body></html>",
+        "noQuirksBodyHtml": "<p><a></a></p><plaintext><a>b</a></plaintext>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><div>a<a></div>b<p>c</p>d",
+      "errors": [
+        "(1,30): end-tag-too-early",
+        "(1,40): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "div": true,
+            "a": true,
+            "p": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "div",
+                    "children": [
+                      {
+                        "text": "a"
+                      },
+                      {
+                        "tag": "a"
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "a",
+                    "children": [
+                      {
+                        "text": "b"
+                      },
+                      {
+                        "tag": "p",
+                        "children": [
+                          {
+                            "text": "c"
+                          }
+                        ]
+                      },
+                      {
+                        "text": "d"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><div>a<a></a></div><a>b<p>c</p>d</a></body></html>",
+        "noQuirksBodyHtml": "<div>a<a></a></div><a>b<p>c</p>d</a>"
+      }
+    }
+  ],
+  "tests2.dat": [
+    {
+      "data": "<!DOCTYPE html>Test",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "Test"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body>Test</body></html>",
+        "noQuirksBodyHtml": "Test"
+      }
+    },
+    {
+      "data": "<textarea>test</div>test",
+      "errors": [
+        "(1,10): expected-doctype-but-got-start-tag",
+        "(1,24): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "textarea": true
+          },
+          "escaped": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "textarea",
+                    "children": [
+                      {
+                        "text": "test</div>test",
+                        "escaped": true
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><textarea>test&lt;/div&gt;test</textarea></body></html>",
+        "noQuirksBodyHtml": "<textarea>test&lt;/div&gt;test</textarea>"
+      }
+    },
+    {
+      "data": "<table><td>",
+      "errors": [
+        "(1,7): expected-doctype-but-got-start-tag",
+        "(1,11): unexpected-cell-in-table-body",
+        "(1,11): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true,
+            "tbody": true,
+            "tr": true,
+            "td": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "tbody",
+                        "children": [
+                          {
+                            "tag": "tr",
+                            "children": [
+                              {
+                                "tag": "td"
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><table><tbody><tr><td></td></tr></tbody></table></body></html>",
+        "noQuirksBodyHtml": "<table><tbody><tr><td></td></tr></tbody></table>"
+      }
+    },
+    {
+      "data": "<table><td>test</tbody></table>",
+      "errors": [
+        "(1,7): expected-doctype-but-got-start-tag",
+        "(1,11): unexpected-cell-in-table-body"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true,
+            "tbody": true,
+            "tr": true,
+            "td": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "tbody",
+                        "children": [
+                          {
+                            "tag": "tr",
+                            "children": [
+                              {
+                                "tag": "td",
+                                "children": [
+                                  {
+                                    "text": "test"
+                                  }
+                                ]
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><table><tbody><tr><td>test</td></tr></tbody></table></body></html>",
+        "noQuirksBodyHtml": "<table><tbody><tr><td>test</td></tr></tbody></table>"
+      }
+    },
+    {
+      "data": "<frame>test",
+      "errors": [
+        "(1,7): expected-doctype-but-got-start-tag",
+        "(1,7): unexpected-start-tag-ignored"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "test"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>test</body></html>",
+        "noQuirksBodyHtml": "test"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><frameset>test",
+      "errors": [
+        "(1,29): unexpected-char-in-frameset",
+        "(1,29): unexpected-char-in-frameset",
+        "(1,29): unexpected-char-in-frameset",
+        "(1,29): unexpected-char-in-frameset",
+        "(1,29): eof-in-frameset"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "frameset": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "frameset"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><frameset></frameset></html>",
+        "noQuirksBodyHtml": "test"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><frameset> te st",
+      "errors": [
+        "(1,29): unexpected-char-in-frameset",
+        "(1,29): unexpected-char-in-frameset",
+        "(1,29): unexpected-char-in-frameset",
+        "(1,29): unexpected-char-in-frameset",
+        "(1,29): eof-in-frameset"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "frameset": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "frameset",
+                "children": [
+                  {
+                    "text": "  "
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><frameset>  </frameset></html>",
+        "noQuirksBodyHtml": " te st"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><frameset></frameset> te st",
+      "errors": [
+        "(1,29): unexpected-char-after-frameset",
+        "(1,29): unexpected-char-after-frameset",
+        "(1,29): unexpected-char-after-frameset",
+        "(1,29): unexpected-char-after-frameset"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "frameset": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "frameset"
+              },
+              {
+                "text": "  "
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><frameset></frameset>  </html>",
+        "noQuirksBodyHtml": " te st"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><frameset><!DOCTYPE html>",
+      "errors": [
+        "(1,40): unexpected-doctype",
+        "(1,40): eof-in-frameset"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "frameset": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "frameset"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><frameset></frameset></html>",
+        "noQuirksBodyHtml": ""
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><font><p><b>test</font>",
+      "errors": [
+        "(1,38): adoption-agency-1.3",
+        "(1,38): adoption-agency-1.3"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "font": true,
+            "p": true,
+            "b": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "font"
+                  },
+                  {
+                    "tag": "p",
+                    "children": [
+                      {
+                        "tag": "font",
+                        "children": [
+                          {
+                            "tag": "b",
+                            "children": [
+                              {
+                                "text": "test"
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><font></font><p><font><b>test</b></font></p></body></html>",
+        "noQuirksBodyHtml": "<font></font><p><font><b>test</b></font></p>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><dt><div><dd>",
+      "errors": [
+        "(1,28): end-tag-too-early"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "dt": true,
+            "div": true,
+            "dd": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "dt",
+                    "children": [
+                      {
+                        "tag": "div"
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "dd"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><dt><div></div></dt><dd></dd></body></html>",
+        "noQuirksBodyHtml": "<dt><div></div></dt><dd></dd>"
+      }
+    },
+    {
+      "data": "<script></x",
+      "errors": [
+        "(1,8): expected-doctype-but-got-start-tag",
+        "(1,11): expected-named-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "</x",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><script></x</script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script></x</script>"
+      }
+    },
+    {
+      "data": "<table><plaintext><td>",
+      "errors": [
+        "(1,7): expected-doctype-but-got-start-tag",
+        "(1,18): unexpected-start-tag-implies-table-voodoo",
+        "(1,22): foster-parenting-character-in-table",
+        "(1,22): foster-parenting-character-in-table",
+        "(1,22): foster-parenting-character-in-table",
+        "(1,22): foster-parenting-character-in-table",
+        "(1,22): eof-in-table"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "plaintext": true,
+            "table": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "plaintext",
+                    "children": [
+                      {
+                        "text": "<td>",
+                        "no_escape": true
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "table"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><plaintext><td></plaintext><table></table></body></html>",
+        "noQuirksBodyHtml": "<plaintext><td></plaintext><table></table>"
+      }
+    },
+    {
+      "data": "<plaintext></plaintext>",
+      "errors": [
+        "(1,11): expected-doctype-but-got-start-tag",
+        "(1,23): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "plaintext": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "plaintext",
+                    "children": [
+                      {
+                        "text": "</plaintext>",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><plaintext></plaintext></plaintext></body></html>",
+        "noQuirksBodyHtml": "<plaintext></plaintext></plaintext>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><table><tr>TEST",
+      "errors": [
+        "(1,30): foster-parenting-character-in-table",
+        "(1,30): foster-parenting-character-in-table",
+        "(1,30): foster-parenting-character-in-table",
+        "(1,30): foster-parenting-character-in-table",
+        "(1,30): eof-in-table"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true,
+            "tbody": true,
+            "tr": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "TEST"
+                  },
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "tbody",
+                        "children": [
+                          {
+                            "tag": "tr"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body>TEST<table><tbody><tr></tr></tbody></table></body></html>",
+        "noQuirksBodyHtml": "TEST<table><tbody><tr></tr></tbody></table>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><body t1=1><body t2=2><body t3=3 t4=4>",
+      "errors": [
+        "(1,37): unexpected-start-tag",
+        "(1,53): unexpected-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "attrs": [
+                  {
+                    "name": "t1",
+                    "value": "1"
+                  },
+                  {
+                    "name": "t2",
+                    "value": "2"
+                  },
+                  {
+                    "name": "t3",
+                    "value": "3"
+                  },
+                  {
+                    "name": "t4",
+                    "value": "4"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body t1=\"1\" t2=\"2\" t3=\"3\" t4=\"4\"></body></html>",
+        "noQuirksBodyHtml": ""
+      }
+    },
+    {
+      "data": "</b test",
+      "errors": [
+        "(1,8): eof-in-attribute-name",
+        "(1,8): expected-doctype-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body></body></html>",
+        "noQuirksBodyHtml": ""
+      }
+    },
+    {
+      "data": "<!DOCTYPE html></b test<b &=&amp>X",
+      "errors": [
+        "(1,24): invalid-character-in-attribute-name",
+        "(1,32): named-entity-without-semicolon",
+        "(1,33): attributes-in-end-tag",
+        "(1,33): unexpected-end-tag-before-html"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "X"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body>X</body></html>",
+        "noQuirksBodyHtml": "X"
+      }
+    },
+    {
+      "data": "<!doctypehtml><scrIPt type=text/x-foobar;baz>X</SCRipt",
+      "errors": [
+        "(1,9): need-space-after-doctype",
+        "(1,54): expected-named-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "doctype": true,
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "attrs": [
+                      {
+                        "name": "type",
+                        "value": "text/x-foobar;baz"
+                      }
+                    ],
+                    "children": [
+                      {
+                        "text": "X</SCRipt",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head><script type=\"text/x-foobar;baz\">X</SCRipt</script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script type=\"text/x-foobar;baz\">X</SCRipt</script>"
+      }
+    },
+    {
+      "data": "&",
+      "errors": [
+        "(1,1): expected-doctype-but-got-chars"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "escaped": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "&",
+                    "escaped": true
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>&amp;</body></html>",
+        "noQuirksBodyHtml": "&amp;"
+      }
+    },
+    {
+      "data": "&#",
+      "errors": [
+        "(1,2): expected-numeric-entity",
+        "(1,2): expected-doctype-but-got-chars"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "escaped": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "&#",
+                    "escaped": true
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>&amp;#</body></html>",
+        "noQuirksBodyHtml": "&amp;#"
+      }
+    },
+    {
+      "data": "&#X",
+      "errors": [
+        "(1,3): expected-numeric-entity",
+        "(1,3): expected-doctype-but-got-chars"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "escaped": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "&#X",
+                    "escaped": true
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>&amp;#X</body></html>",
+        "noQuirksBodyHtml": "&amp;#X"
+      }
+    },
+    {
+      "data": "&#x",
+      "errors": [
+        "(1,3): expected-numeric-entity",
+        "(1,3): expected-doctype-but-got-chars"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "escaped": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "&#x",
+                    "escaped": true
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>&amp;#x</body></html>",
+        "noQuirksBodyHtml": "&amp;#x"
+      }
+    },
+    {
+      "data": "&#45",
+      "errors": [
+        "(1,4): numeric-entity-without-semicolon",
+        "(1,4): expected-doctype-but-got-chars"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "-"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>-</body></html>",
+        "noQuirksBodyHtml": "-"
+      }
+    },
+    {
+      "data": "&x-test",
+      "errors": [
+        "(1,2): expected-doctype-but-got-chars"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "escaped": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "&x-test",
+                    "escaped": true
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>&amp;x-test</body></html>",
+        "noQuirksBodyHtml": "&amp;x-test"
+      }
+    },
+    {
+      "data": "<!doctypehtml><p><li>",
+      "errors": [
+        "(1,9): need-space-after-doctype"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "p": true,
+            "li": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "p"
+                  },
+                  {
+                    "tag": "li"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><p></p><li></li></body></html>",
+        "noQuirksBodyHtml": "<p></p><li></li>"
+      }
+    },
+    {
+      "data": "<!doctypehtml><p><dt>",
+      "errors": [
+        "(1,9): need-space-after-doctype"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "p": true,
+            "dt": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "p"
+                  },
+                  {
+                    "tag": "dt"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><p></p><dt></dt></body></html>",
+        "noQuirksBodyHtml": "<p></p><dt></dt>"
+      }
+    },
+    {
+      "data": "<!doctypehtml><p><dd>",
+      "errors": [
+        "(1,9): need-space-after-doctype"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "p": true,
+            "dd": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "p"
+                  },
+                  {
+                    "tag": "dd"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><p></p><dd></dd></body></html>",
+        "noQuirksBodyHtml": "<p></p><dd></dd>"
+      }
+    },
+    {
+      "data": "<!doctypehtml><p><form>",
+      "errors": [
+        "(1,9): need-space-after-doctype",
+        "(1,23): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "p": true,
+            "form": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "p"
+                  },
+                  {
+                    "tag": "form"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><p></p><form></form></body></html>",
+        "noQuirksBodyHtml": "<p></p><form></form>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><p></P>X",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "p": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "p"
+                  },
+                  {
+                    "text": "X"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><p></p>X</body></html>",
+        "noQuirksBodyHtml": "<p></p>X"
+      }
+    },
+    {
+      "data": "&AMP",
+      "errors": [
+        "(1,4): named-entity-without-semicolon",
+        "(1,4): expected-doctype-but-got-chars"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "escaped": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "&",
+                    "escaped": true
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>&amp;</body></html>",
+        "noQuirksBodyHtml": "&amp;"
+      }
+    },
+    {
+      "data": "&AMp;",
+      "errors": [
+        "(1,3): expected-named-entity",
+        "(1,3): expected-doctype-but-got-chars"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "escaped": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "&AMp;",
+                    "escaped": true
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>&amp;AMp;</body></html>",
+        "noQuirksBodyHtml": "&amp;AMp;"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><html><head></head><body><thisISasillyTESTelementNameToMakeSureCrazyTagNamesArePARSEDcorrectLY>",
+      "errors": [
+        "(1,110): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "thisisasillytestelementnametomakesurecrazytagnamesareparsedcorrectly": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "thisisasillytestelementnametomakesurecrazytagnamesareparsedcorrectly"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><thisisasillytestelementnametomakesurecrazytagnamesareparsedcorrectly></thisisasillytestelementnametomakesurecrazytagnamesareparsedcorrectly></body></html>",
+        "noQuirksBodyHtml": "<thisisasillytestelementnametomakesurecrazytagnamesareparsedcorrectly></thisisasillytestelementnametomakesurecrazytagnamesareparsedcorrectly>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html>X</body>X",
+      "errors": [
+        "(1,24): unexpected-char-after-body"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "XX"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body>XX</body></html>",
+        "noQuirksBodyHtml": "XX"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><!-- X",
+      "errors": [
+        "(1,21): eof-in-comment"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "doctype": true,
+          "comment": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "comment": " X"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><!-- X--><html><head></head><body></body></html>",
+        "noQuirksBodyHtml": "<!-- X-->"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><table><caption>test TEST</caption><td>test",
+      "errors": [
+        "(1,54): unexpected-cell-in-table-body",
+        "(1,58): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true,
+            "caption": true,
+            "tbody": true,
+            "tr": true,
+            "td": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "caption",
+                        "children": [
+                          {
+                            "text": "test TEST"
+                          }
+                        ]
+                      },
+                      {
+                        "tag": "tbody",
+                        "children": [
+                          {
+                            "tag": "tr",
+                            "children": [
+                              {
+                                "tag": "td",
+                                "children": [
+                                  {
+                                    "text": "test"
+                                  }
+                                ]
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><table><caption>test TEST</caption><tbody><tr><td>test</td></tr></tbody></table></body></html>",
+        "noQuirksBodyHtml": "<table><caption>test TEST</caption><tbody><tr><td>test</td></tr></tbody></table>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><select><option><optgroup>",
+      "errors": [
+        "(1,41): eof-in-select"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "select": true,
+            "option": true,
+            "optgroup": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "select",
+                    "children": [
+                      {
+                        "tag": "option"
+                      },
+                      {
+                        "tag": "optgroup"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><select><option></option><optgroup></optgroup></select></body></html>",
+        "noQuirksBodyHtml": "<select><option></option><optgroup></optgroup></select>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><select><optgroup><option></optgroup><option><select><option>",
+      "errors": [
+        "(1,68): unexpected-select-in-select"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "select": true,
+            "optgroup": true,
+            "option": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "select",
+                    "children": [
+                      {
+                        "tag": "optgroup",
+                        "children": [
+                          {
+                            "tag": "option"
+                          }
+                        ]
+                      },
+                      {
+                        "tag": "option"
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "option"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><select><optgroup><option></option></optgroup><option></option></select><option></option></body></html>",
+        "noQuirksBodyHtml": "<select><optgroup><option></option></optgroup><option></option></select><option></option>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><select><optgroup><option><optgroup>",
+      "errors": [
+        "(1,51): eof-in-select"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "select": true,
+            "optgroup": true,
+            "option": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "select",
+                    "children": [
+                      {
+                        "tag": "optgroup",
+                        "children": [
+                          {
+                            "tag": "option"
+                          }
+                        ]
+                      },
+                      {
+                        "tag": "optgroup"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><select><optgroup><option></option></optgroup><optgroup></optgroup></select></body></html>",
+        "noQuirksBodyHtml": "<select><optgroup><option></option></optgroup><optgroup></optgroup></select>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><datalist><option>foo</datalist>bar",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "datalist": true,
+            "option": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "datalist",
+                    "children": [
+                      {
+                        "tag": "option",
+                        "children": [
+                          {
+                            "text": "foo"
+                          }
+                        ]
+                      }
+                    ]
+                  },
+                  {
+                    "text": "bar"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><datalist><option>foo</option></datalist>bar</body></html>",
+        "noQuirksBodyHtml": "<datalist><option>foo</option></datalist>bar"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><font><input><input></font>",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "font": true,
+            "input": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "font",
+                    "children": [
+                      {
+                        "tag": "input"
+                      },
+                      {
+                        "tag": "input"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><font><input><input></font></body></html>",
+        "noQuirksBodyHtml": "<font><input><input></font>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><!-- XXX - XXX -->",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "doctype": true,
+          "comment": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "comment": " XXX - XXX "
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><!-- XXX - XXX --><html><head></head><body></body></html>",
+        "noQuirksBodyHtml": "<!-- XXX - XXX -->"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><!-- XXX - XXX",
+      "errors": [
+        "(1,29): eof-in-comment"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "doctype": true,
+          "comment": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "comment": " XXX - XXX"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><!-- XXX - XXX--><html><head></head><body></body></html>",
+        "noQuirksBodyHtml": "<!-- XXX - XXX-->"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><!-- XXX - XXX - XXX -->",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "doctype": true,
+          "comment": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "comment": " XXX - XXX - XXX "
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><!-- XXX - XXX - XXX --><html><head></head><body></body></html>",
+        "noQuirksBodyHtml": "<!-- XXX - XXX - XXX -->"
+      }
+    },
+    {
+      "data": "test\ntest",
+      "errors": [
+        "(2,4): expected-doctype-but-got-chars"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "test\ntest"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>test\ntest</body></html>",
+        "noQuirksBodyHtml": "test\ntest"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><body><title>test</body></title>",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "title": true
+          },
+          "doctype": true,
+          "escaped": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "title",
+                    "children": [
+                      {
+                        "text": "test</body>",
+                        "escaped": true
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><title>test&lt;/body&gt;</title></body></html>",
+        "noQuirksBodyHtml": "<title>test&lt;/body&gt;</title>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><body><title>X</title><meta name=z><link rel=foo><style>\nx { content:\"</style\" } </style>",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "title": true,
+            "meta": true,
+            "link": true,
+            "style": true
+          },
+          "doctype": true,
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "title",
+                    "children": [
+                      {
+                        "text": "X"
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "meta",
+                    "attrs": [
+                      {
+                        "name": "name",
+                        "value": "z"
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "link",
+                    "attrs": [
+                      {
+                        "name": "rel",
+                        "value": "foo"
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "style",
+                    "children": [
+                      {
+                        "text": "\nx { content:\"</style\" } ",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><title>X</title><meta name=\"z\"><link rel=\"foo\"><style>\nx { content:\"</style\" } </style></body></html>",
+        "noQuirksBodyHtml": "<title>X</title><meta name=\"z\"><link rel=\"foo\"><style>\nx { content:\"</style\" } </style>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><select><optgroup></optgroup></select>",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "select": true,
+            "optgroup": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "select",
+                    "children": [
+                      {
+                        "tag": "optgroup"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><select><optgroup></optgroup></select></body></html>",
+        "noQuirksBodyHtml": "<select><optgroup></optgroup></select>"
+      }
+    },
+    {
+      "data": " \n ",
+      "errors": [
+        "(2,1): expected-doctype-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body></body></html>",
+        "noQuirksBodyHtml": " \n "
+      }
+    },
+    {
+      "data": "<!DOCTYPE html>  <html>",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body></body></html>",
+        "noQuirksBodyHtml": "  "
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><script>\n</script>  <title>x</title>  </head>",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "title": true,
+            "body": true
+          },
+          "doctype": true,
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "\n",
+                        "no_escape": true
+                      }
+                    ]
+                  },
+                  {
+                    "text": "  "
+                  },
+                  {
+                    "tag": "title",
+                    "children": [
+                      {
+                        "text": "x"
+                      }
+                    ]
+                  },
+                  {
+                    "text": "  "
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head><script>\n</script>  <title>x</title>  </head><body></body></html>",
+        "noQuirksBodyHtml": "<script>\n</script>  <title>x</title>  "
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><html><body><html id=x>",
+      "errors": [
+        "(1,38): non-html-root"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "attrs": [
+              {
+                "name": "id",
+                "value": "x"
+              }
+            ],
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html id=\"x\"><head></head><body></body></html>",
+        "noQuirksBodyHtml": ""
+      }
+    },
+    {
+      "data": "<!DOCTYPE html>X</body><html id=\"x\">",
+      "errors": [
+        "(1,36): non-html-root"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "attrs": [
+              {
+                "name": "id",
+                "value": "x"
+              }
+            ],
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "X"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html id=\"x\"><head></head><body>X</body></html>",
+        "noQuirksBodyHtml": "X"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><head><html id=x>",
+      "errors": [
+        "(1,32): non-html-root"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "attrs": [
+              {
+                "name": "id",
+                "value": "x"
+              }
+            ],
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html id=\"x\"><head></head><body></body></html>",
+        "noQuirksBodyHtml": ""
+      }
+    },
+    {
+      "data": "<!DOCTYPE html>X</html>X",
+      "errors": [
+        "(1,24): expected-eof-but-got-char"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "XX"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body>XX</body></html>",
+        "noQuirksBodyHtml": "XX"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html>X</html> ",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "X "
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body>X </body></html>",
+        "noQuirksBodyHtml": "X "
+      }
+    },
+    {
+      "data": "<!DOCTYPE html>X</html><p>X",
+      "errors": [
+        "(1,26): expected-eof-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "p": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "X"
+                  },
+                  {
+                    "tag": "p",
+                    "children": [
+                      {
+                        "text": "X"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body>X<p>X</p></body></html>",
+        "noQuirksBodyHtml": "X<p>X</p>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html>X<p/x/y/z>",
+      "errors": [
+        "(1,19): unexpected-character-after-solidus-in-tag",
+        "(1,21): unexpected-character-after-solidus-in-tag",
+        "(1,23): unexpected-character-after-solidus-in-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "p": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "X"
+                  },
+                  {
+                    "tag": "p",
+                    "attrs": [
+                      {
+                        "name": "x",
+                        "value": ""
+                      },
+                      {
+                        "name": "y",
+                        "value": ""
+                      },
+                      {
+                        "name": "z",
+                        "value": ""
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body>X<p x=\"\" y=\"\" z=\"\"></p></body></html>",
+        "noQuirksBodyHtml": "X<p x=\"\" y=\"\" z=\"\"></p>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><!--x--",
+      "errors": [
+        "(1,22): eof-in-comment-double-dash"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "doctype": true,
+          "comment": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "comment": "x"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><!--x--><html><head></head><body></body></html>",
+        "noQuirksBodyHtml": "<!--x-->"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><table><tr><td></p></table>",
+      "errors": [
+        "(1,34): unexpected-end-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true,
+            "tbody": true,
+            "tr": true,
+            "td": true,
+            "p": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "tbody",
+                        "children": [
+                          {
+                            "tag": "tr",
+                            "children": [
+                              {
+                                "tag": "td",
+                                "children": [
+                                  {
+                                    "tag": "p"
+                                  }
+                                ]
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><table><tbody><tr><td><p></p></td></tr></tbody></table></body></html>",
+        "noQuirksBodyHtml": "<table><tbody><tr><td><p></p></td></tr></tbody></table>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE <!DOCTYPE HTML>><!--<!--x-->-->",
+      "errors": [
+        "(1,20): expected-space-or-right-bracket-in-doctype",
+        "(1,25): unknown-doctype",
+        "(1,35): unexpected-char-in-comment"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "doctype": true,
+          "escaped": true,
+          "comment": true
+        },
+        "tree": [
+          {
+            "doctype": "<!doctype"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": ">",
+                    "escaped": true
+                  },
+                  {
+                    "comment": "<!--x"
+                  },
+                  {
+                    "text": "-->",
+                    "escaped": true
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE <!doctype><html><head></head><body>&gt;<!--<!--x-->--&gt;</body></html>",
+        "noQuirksBodyHtml": "&gt;<!--<!--x-->--&gt;"
+      }
+    },
+    {
+      "data": "<!doctype html><div><form></form><div></div></div>",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "div": true,
+            "form": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "div",
+                    "children": [
+                      {
+                        "tag": "form"
+                      },
+                      {
+                        "tag": "div"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><div><form></form><div></div></div></body></html>",
+        "noQuirksBodyHtml": "<div><form></form><div></div></div>"
+      }
+    }
+  ],
+  "tests20.dat": [
+    {
+      "data": "<!doctype html><p><button><button>",
+      "errors": [
+        "(1,34): unexpected-start-tag-implies-end-tag",
+        "(1,34): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "p": true,
+            "button": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "p",
+                    "children": [
+                      {
+                        "tag": "button"
+                      },
+                      {
+                        "tag": "button"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><p><button></button><button></button></p></body></html>",
+        "noQuirksBodyHtml": "<p><button></button><button></button></p>"
+      }
+    },
+    {
+      "data": "<!doctype html><p><button><address>",
+      "errors": [
+        "(1,35): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "p": true,
+            "button": true,
+            "address": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "p",
+                    "children": [
+                      {
+                        "tag": "button",
+                        "children": [
+                          {
+                            "tag": "address"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><p><button><address></address></button></p></body></html>",
+        "noQuirksBodyHtml": "<p><button><address></address></button></p>"
+      }
+    },
+    {
+      "data": "<!doctype html><p><button><blockquote>",
+      "errors": [
+        "(1,38): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "p": true,
+            "button": true,
+            "blockquote": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "p",
+                    "children": [
+                      {
+                        "tag": "button",
+                        "children": [
+                          {
+                            "tag": "blockquote"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><p><button><blockquote></blockquote></button></p></body></html>",
+        "noQuirksBodyHtml": "<p><button><blockquote></blockquote></button></p>"
+      }
+    },
+    {
+      "data": "<!doctype html><p><button><menu>",
+      "errors": [
+        "(1,32): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "p": true,
+            "button": true,
+            "menu": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "p",
+                    "children": [
+                      {
+                        "tag": "button",
+                        "children": [
+                          {
+                            "tag": "menu"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><p><button><menu></menu></button></p></body></html>",
+        "noQuirksBodyHtml": "<p><button><menu></menu></button></p>"
+      }
+    },
+    {
+      "data": "<!doctype html><p><button><p>",
+      "errors": [
+        "(1,29): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "p": true,
+            "button": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "p",
+                    "children": [
+                      {
+                        "tag": "button",
+                        "children": [
+                          {
+                            "tag": "p"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><p><button><p></p></button></p></body></html>",
+        "noQuirksBodyHtml": "<p><button><p></p></button></p>"
+      }
+    },
+    {
+      "data": "<!doctype html><p><button><ul>",
+      "errors": [
+        "(1,30): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "p": true,
+            "button": true,
+            "ul": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "p",
+                    "children": [
+                      {
+                        "tag": "button",
+                        "children": [
+                          {
+                            "tag": "ul"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><p><button><ul></ul></button></p></body></html>",
+        "noQuirksBodyHtml": "<p><button><ul></ul></button></p>"
+      }
+    },
+    {
+      "data": "<!doctype html><p><button><h1>",
+      "errors": [
+        "(1,30): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "p": true,
+            "button": true,
+            "h1": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "p",
+                    "children": [
+                      {
+                        "tag": "button",
+                        "children": [
+                          {
+                            "tag": "h1"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><p><button><h1></h1></button></p></body></html>",
+        "noQuirksBodyHtml": "<p><button><h1></h1></button></p>"
+      }
+    },
+    {
+      "data": "<!doctype html><p><button><h6>",
+      "errors": [
+        "(1,30): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "p": true,
+            "button": true,
+            "h6": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "p",
+                    "children": [
+                      {
+                        "tag": "button",
+                        "children": [
+                          {
+                            "tag": "h6"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><p><button><h6></h6></button></p></body></html>",
+        "noQuirksBodyHtml": "<p><button><h6></h6></button></p>"
+      }
+    },
+    {
+      "data": "<!doctype html><p><button><listing>",
+      "errors": [
+        "(1,35): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "p": true,
+            "button": true,
+            "listing": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "p",
+                    "children": [
+                      {
+                        "tag": "button",
+                        "children": [
+                          {
+                            "tag": "listing"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><p><button><listing></listing></button></p></body></html>",
+        "noQuirksBodyHtml": "<p><button><listing></listing></button></p>"
+      }
+    },
+    {
+      "data": "<!doctype html><p><button><pre>",
+      "errors": [
+        "(1,31): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "p": true,
+            "button": true,
+            "pre": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "p",
+                    "children": [
+                      {
+                        "tag": "button",
+                        "children": [
+                          {
+                            "tag": "pre"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><p><button><pre></pre></button></p></body></html>",
+        "noQuirksBodyHtml": "<p><button><pre></pre></button></p>"
+      }
+    },
+    {
+      "data": "<!doctype html><p><button><form>",
+      "errors": [
+        "(1,32): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "p": true,
+            "button": true,
+            "form": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "p",
+                    "children": [
+                      {
+                        "tag": "button",
+                        "children": [
+                          {
+                            "tag": "form"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><p><button><form></form></button></p></body></html>",
+        "noQuirksBodyHtml": "<p><button><form></form></button></p>"
+      }
+    },
+    {
+      "data": "<!doctype html><p><button><li>",
+      "errors": [
+        "(1,30): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "p": true,
+            "button": true,
+            "li": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "p",
+                    "children": [
+                      {
+                        "tag": "button",
+                        "children": [
+                          {
+                            "tag": "li"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><p><button><li></li></button></p></body></html>",
+        "noQuirksBodyHtml": "<p><button><li></li></button></p>"
+      }
+    },
+    {
+      "data": "<!doctype html><p><button><dd>",
+      "errors": [
+        "(1,30): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "p": true,
+            "button": true,
+            "dd": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "p",
+                    "children": [
+                      {
+                        "tag": "button",
+                        "children": [
+                          {
+                            "tag": "dd"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><p><button><dd></dd></button></p></body></html>",
+        "noQuirksBodyHtml": "<p><button><dd></dd></button></p>"
+      }
+    },
+    {
+      "data": "<!doctype html><p><button><dt>",
+      "errors": [
+        "(1,30): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "p": true,
+            "button": true,
+            "dt": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "p",
+                    "children": [
+                      {
+                        "tag": "button",
+                        "children": [
+                          {
+                            "tag": "dt"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><p><button><dt></dt></button></p></body></html>",
+        "noQuirksBodyHtml": "<p><button><dt></dt></button></p>"
+      }
+    },
+    {
+      "data": "<!doctype html><p><button><plaintext>",
+      "errors": [
+        "(1,37): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "p": true,
+            "button": true,
+            "plaintext": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "p",
+                    "children": [
+                      {
+                        "tag": "button",
+                        "children": [
+                          {
+                            "tag": "plaintext"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><p><button><plaintext></plaintext></button></p></body></html>",
+        "noQuirksBodyHtml": "<p><button><plaintext></plaintext></button></p>"
+      }
+    },
+    {
+      "data": "<!doctype html><p><button><table>",
+      "errors": [
+        "(1,33): eof-in-table"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "p": true,
+            "button": true,
+            "table": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "p",
+                    "children": [
+                      {
+                        "tag": "button",
+                        "children": [
+                          {
+                            "tag": "table"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><p><button><table></table></button></p></body></html>",
+        "noQuirksBodyHtml": "<p><button><table></table></button></p>"
+      }
+    },
+    {
+      "data": "<!doctype html><p><button><hr>",
+      "errors": [
+        "(1,30): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "p": true,
+            "button": true,
+            "hr": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "p",
+                    "children": [
+                      {
+                        "tag": "button",
+                        "children": [
+                          {
+                            "tag": "hr"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><p><button><hr></button></p></body></html>",
+        "noQuirksBodyHtml": "<p><button><hr></button></p>"
+      }
+    },
+    {
+      "data": "<!doctype html><p><button><xmp>",
+      "errors": [
+        "(1,31): expected-named-closing-tag-but-got-eof",
+        "(1,31): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "p": true,
+            "button": true,
+            "xmp": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "p",
+                    "children": [
+                      {
+                        "tag": "button",
+                        "children": [
+                          {
+                            "tag": "xmp"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><p><button><xmp></xmp></button></p></body></html>",
+        "noQuirksBodyHtml": "<p><button><xmp></xmp></button></p>"
+      }
+    },
+    {
+      "data": "<!doctype html><p><button></p>",
+      "errors": [
+        "(1,30): unexpected-end-tag",
+        "(1,30): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "p": true,
+            "button": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "p",
+                    "children": [
+                      {
+                        "tag": "button",
+                        "children": [
+                          {
+                            "tag": "p"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><p><button><p></p></button></p></body></html>",
+        "noQuirksBodyHtml": "<p><button><p></p></button></p>"
+      }
+    },
+    {
+      "data": "<!doctype html><address><button></address>a",
+      "errors": [
+        "(1,42): end-tag-too-early"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "address": true,
+            "button": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "address",
+                    "children": [
+                      {
+                        "tag": "button"
+                      }
+                    ]
+                  },
+                  {
+                    "text": "a"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><address><button></button></address>a</body></html>",
+        "noQuirksBodyHtml": "<address><button></button></address>a"
+      }
+    },
+    {
+      "data": "<!doctype html><address><button></address>a",
+      "errors": [
+        "(1,42): end-tag-too-early"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "address": true,
+            "button": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "address",
+                    "children": [
+                      {
+                        "tag": "button"
+                      }
+                    ]
+                  },
+                  {
+                    "text": "a"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><address><button></button></address>a</body></html>",
+        "noQuirksBodyHtml": "<address><button></button></address>a"
+      }
+    },
+    {
+      "data": "<p><table></p>",
+      "errors": [
+        "(1,3): expected-doctype-but-got-start-tag",
+        "(1,14): unexpected-end-tag-implies-table-voodoo",
+        "(1,14): unexpected-end-tag",
+        "(1,14): eof-in-table"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "p": true,
+            "table": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "p",
+                    "children": [
+                      {
+                        "tag": "p"
+                      },
+                      {
+                        "tag": "table"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><p><p></p><table></table></p></body></html>",
+        "noQuirksBodyHtml": "<p></p><p></p><table></table>"
+      }
+    },
+    {
+      "data": "<!doctype html><svg>",
+      "errors": [
+        "(1,20): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "svg svg": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "svg",
+                    "ns": "http://www.w3.org/2000/svg"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><svg></svg></body></html>",
+        "noQuirksBodyHtml": "<svg></svg>"
+      }
+    },
+    {
+      "data": "<!doctype html><p><figcaption>",
+      "errors": [
+        "(1,30): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "p": true,
+            "figcaption": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "p"
+                  },
+                  {
+                    "tag": "figcaption"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><p></p><figcaption></figcaption></body></html>",
+        "noQuirksBodyHtml": "<p></p><figcaption></figcaption>"
+      }
+    },
+    {
+      "data": "<!doctype html><p><summary>",
+      "errors": [
+        "(1,27): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "p": true,
+            "summary": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "p"
+                  },
+                  {
+                    "tag": "summary"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><p></p><summary></summary></body></html>",
+        "noQuirksBodyHtml": "<p></p><summary></summary>"
+      }
+    },
+    {
+      "data": "<!doctype html><form><table><form>",
+      "errors": [
+        "(1,34): unexpected-form-in-table",
+        "(1,34): eof-in-table"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "form": true,
+            "table": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "form",
+                    "children": [
+                      {
+                        "tag": "table"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><form><table></table></form></body></html>",
+        "noQuirksBodyHtml": "<form><table></table></form>"
+      }
+    },
+    {
+      "data": "<!doctype html><table><form><form>",
+      "errors": [
+        "(1,28): unexpected-form-in-table",
+        "(1,34): unexpected-form-in-table",
+        "(1,34): eof-in-table"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true,
+            "form": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "form"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><table><form></form></table></body></html>",
+        "noQuirksBodyHtml": "<table><form></form></table>"
+      }
+    },
+    {
+      "data": "<!doctype html><table><form></table><form>",
+      "errors": [
+        "(1,28): unexpected-form-in-table",
+        "(1,42): unexpected-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true,
+            "form": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "form"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><table><form></form></table></body></html>",
+        "noQuirksBodyHtml": "<table><form></form></table>"
+      }
+    },
+    {
+      "data": "<!doctype html><svg><foreignObject><p>",
+      "errors": [
+        "(1,38): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "svg svg": true,
+            "svg foreignObject": true,
+            "p": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "svg",
+                    "ns": "http://www.w3.org/2000/svg",
+                    "children": [
+                      {
+                        "tag": "foreignObject",
+                        "ns": "http://www.w3.org/2000/svg",
+                        "children": [
+                          {
+                            "tag": "p"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><svg><foreignObject><p></p></foreignObject></svg></body></html>",
+        "noQuirksBodyHtml": "<svg><foreignObject><p></p></foreignObject></svg>"
+      }
+    },
+    {
+      "data": "<!doctype html><svg><title>abc",
+      "errors": [
+        "(1,30): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "svg svg": true,
+            "svg title": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "svg",
+                    "ns": "http://www.w3.org/2000/svg",
+                    "children": [
+                      {
+                        "tag": "title",
+                        "ns": "http://www.w3.org/2000/svg",
+                        "children": [
+                          {
+                            "text": "abc"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><svg><title>abc</title></svg></body></html>",
+        "noQuirksBodyHtml": "<svg><title>abc</title></svg>"
+      }
+    },
+    {
+      "data": "<option><span><option>",
+      "errors": [
+        "(1,8): expected-doctype-but-got-start-tag",
+        "(1,22): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "option": true,
+            "span": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "option",
+                    "children": [
+                      {
+                        "tag": "span",
+                        "children": [
+                          {
+                            "tag": "option"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><option><span><option></option></span></option></body></html>",
+        "noQuirksBodyHtml": "<option><span><option></option></span></option>"
+      }
+    },
+    {
+      "data": "<option><option>",
+      "errors": [
+        "(1,8): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "option": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "option"
+                  },
+                  {
+                    "tag": "option"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><option></option><option></option></body></html>",
+        "noQuirksBodyHtml": "<option></option><option></option>"
+      }
+    },
+    {
+      "data": "<math><annotation-xml><div>",
+      "errors": [
+        "(1,6): expected-doctype-but-got-start-tag",
+        "(1,27): unexpected-html-element-in-foreign-content",
+        "(1,27): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "math math": true,
+            "math annotation-xml": true,
+            "div": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "math",
+                    "ns": "http://www.w3.org/1998/Math/MathML",
+                    "children": [
+                      {
+                        "tag": "annotation-xml",
+                        "ns": "http://www.w3.org/1998/Math/MathML"
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "div"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><math><annotation-xml></annotation-xml></math><div></div></body></html>",
+        "noQuirksBodyHtml": "<math><annotation-xml><div></div></annotation-xml></math>"
+      }
+    },
+    {
+      "data": "<math><annotation-xml encoding=\"application/svg+xml\"><div>",
+      "errors": [
+        "(1,6): expected-doctype-but-got-start-tag",
+        "(1,58): unexpected-html-element-in-foreign-content",
+        "(1,58): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "math math": true,
+            "math annotation-xml": true,
+            "div": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "math",
+                    "ns": "http://www.w3.org/1998/Math/MathML",
+                    "children": [
+                      {
+                        "tag": "annotation-xml",
+                        "ns": "http://www.w3.org/1998/Math/MathML",
+                        "attrs": [
+                          {
+                            "name": "encoding",
+                            "value": "application/svg+xml"
+                          }
+                        ]
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "div"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><math><annotation-xml encoding=\"application/svg+xml\"></annotation-xml></math><div></div></body></html>",
+        "noQuirksBodyHtml": "<math><annotation-xml encoding=\"application/svg+xml\"><div></div></annotation-xml></math>"
+      }
+    },
+    {
+      "data": "<math><annotation-xml encoding=\"application/xhtml+xml\"><div>",
+      "errors": [
+        "(1,6): expected-doctype-but-got-start-tag",
+        "(1,60): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "math math": true,
+            "math annotation-xml": true,
+            "div": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "math",
+                    "ns": "http://www.w3.org/1998/Math/MathML",
+                    "children": [
+                      {
+                        "tag": "annotation-xml",
+                        "ns": "http://www.w3.org/1998/Math/MathML",
+                        "attrs": [
+                          {
+                            "name": "encoding",
+                            "value": "application/xhtml+xml"
+                          }
+                        ],
+                        "children": [
+                          {
+                            "tag": "div"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><math><annotation-xml encoding=\"application/xhtml+xml\"><div></div></annotation-xml></math></body></html>",
+        "noQuirksBodyHtml": "<math><annotation-xml encoding=\"application/xhtml+xml\"><div></div></annotation-xml></math>"
+      }
+    },
+    {
+      "data": "<math><annotation-xml encoding=\"aPPlication/xhtmL+xMl\"><div>",
+      "errors": [
+        "(1,6): expected-doctype-but-got-start-tag",
+        "(1,60): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "math math": true,
+            "math annotation-xml": true,
+            "div": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "math",
+                    "ns": "http://www.w3.org/1998/Math/MathML",
+                    "children": [
+                      {
+                        "tag": "annotation-xml",
+                        "ns": "http://www.w3.org/1998/Math/MathML",
+                        "attrs": [
+                          {
+                            "name": "encoding",
+                            "value": "aPPlication/xhtmL+xMl"
+                          }
+                        ],
+                        "children": [
+                          {
+                            "tag": "div"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><math><annotation-xml encoding=\"aPPlication/xhtmL+xMl\"><div></div></annotation-xml></math></body></html>",
+        "noQuirksBodyHtml": "<math><annotation-xml encoding=\"aPPlication/xhtmL+xMl\"><div></div></annotation-xml></math>"
+      }
+    },
+    {
+      "data": "<math><annotation-xml encoding=\"text/html\"><div>",
+      "errors": [
+        "(1,6): expected-doctype-but-got-start-tag",
+        "(1,48): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "math math": true,
+            "math annotation-xml": true,
+            "div": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "math",
+                    "ns": "http://www.w3.org/1998/Math/MathML",
+                    "children": [
+                      {
+                        "tag": "annotation-xml",
+                        "ns": "http://www.w3.org/1998/Math/MathML",
+                        "attrs": [
+                          {
+                            "name": "encoding",
+                            "value": "text/html"
+                          }
+                        ],
+                        "children": [
+                          {
+                            "tag": "div"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><math><annotation-xml encoding=\"text/html\"><div></div></annotation-xml></math></body></html>",
+        "noQuirksBodyHtml": "<math><annotation-xml encoding=\"text/html\"><div></div></annotation-xml></math>"
+      }
+    },
+    {
+      "data": "<math><annotation-xml encoding=\"Text/htmL\"><div>",
+      "errors": [
+        "(1,6): expected-doctype-but-got-start-tag",
+        "(1,48): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "math math": true,
+            "math annotation-xml": true,
+            "div": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "math",
+                    "ns": "http://www.w3.org/1998/Math/MathML",
+                    "children": [
+                      {
+                        "tag": "annotation-xml",
+                        "ns": "http://www.w3.org/1998/Math/MathML",
+                        "attrs": [
+                          {
+                            "name": "encoding",
+                            "value": "Text/htmL"
+                          }
+                        ],
+                        "children": [
+                          {
+                            "tag": "div"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><math><annotation-xml encoding=\"Text/htmL\"><div></div></annotation-xml></math></body></html>",
+        "noQuirksBodyHtml": "<math><annotation-xml encoding=\"Text/htmL\"><div></div></annotation-xml></math>"
+      }
+    },
+    {
+      "data": "<math><annotation-xml encoding=\" text/html \"><div>",
+      "errors": [
+        "(1,6): expected-doctype-but-got-start-tag",
+        "(1,50): unexpected-html-element-in-foreign-content",
+        "(1,50): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "math math": true,
+            "math annotation-xml": true,
+            "div": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "math",
+                    "ns": "http://www.w3.org/1998/Math/MathML",
+                    "children": [
+                      {
+                        "tag": "annotation-xml",
+                        "ns": "http://www.w3.org/1998/Math/MathML",
+                        "attrs": [
+                          {
+                            "name": "encoding",
+                            "value": " text/html "
+                          }
+                        ]
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "div"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><math><annotation-xml encoding=\" text/html \"></annotation-xml></math><div></div></body></html>",
+        "noQuirksBodyHtml": "<math><annotation-xml encoding=\" text/html \"><div></div></annotation-xml></math>"
+      }
+    },
+    {
+      "data": "<math><annotation-xml> </annotation-xml>",
+      "errors": [
+        "(1,6): expected-doctype-but-got-start-tag",
+        "(1,40): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "math math": true,
+            "math annotation-xml": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "math",
+                    "ns": "http://www.w3.org/1998/Math/MathML",
+                    "children": [
+                      {
+                        "tag": "annotation-xml",
+                        "ns": "http://www.w3.org/1998/Math/MathML",
+                        "children": [
+                          {
+                            "text": " "
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><math><annotation-xml> </annotation-xml></math></body></html>",
+        "noQuirksBodyHtml": "<math><annotation-xml> </annotation-xml></math>"
+      }
+    },
+    {
+      "data": "<math><annotation-xml>c</annotation-xml>",
+      "errors": [
+        "(1,6): expected-doctype-but-got-start-tag",
+        "(1,40): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "math math": true,
+            "math annotation-xml": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "math",
+                    "ns": "http://www.w3.org/1998/Math/MathML",
+                    "children": [
+                      {
+                        "tag": "annotation-xml",
+                        "ns": "http://www.w3.org/1998/Math/MathML",
+                        "children": [
+                          {
+                            "text": "c"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><math><annotation-xml>c</annotation-xml></math></body></html>",
+        "noQuirksBodyHtml": "<math><annotation-xml>c</annotation-xml></math>"
+      }
+    },
+    {
+      "data": "<math><annotation-xml><!--foo-->",
+      "errors": [
+        "(1,6): expected-doctype-but-got-start-tag",
+        "(1,32): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "math math": true,
+            "math annotation-xml": true
+          },
+          "comment": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "math",
+                    "ns": "http://www.w3.org/1998/Math/MathML",
+                    "children": [
+                      {
+                        "tag": "annotation-xml",
+                        "ns": "http://www.w3.org/1998/Math/MathML",
+                        "children": [
+                          {
+                            "comment": "foo"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><math><annotation-xml><!--foo--></annotation-xml></math></body></html>",
+        "noQuirksBodyHtml": "<math><annotation-xml><!--foo--></annotation-xml></math>"
+      }
+    },
+    {
+      "data": "<math><annotation-xml></svg>x",
+      "errors": [
+        "(1,6): expected-doctype-but-got-start-tag",
+        "(1,28): unexpected-end-tag",
+        "(1,29): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "math math": true,
+            "math annotation-xml": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "math",
+                    "ns": "http://www.w3.org/1998/Math/MathML",
+                    "children": [
+                      {
+                        "tag": "annotation-xml",
+                        "ns": "http://www.w3.org/1998/Math/MathML",
+                        "children": [
+                          {
+                            "text": "x"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><math><annotation-xml>x</annotation-xml></math></body></html>",
+        "noQuirksBodyHtml": "<math><annotation-xml>x</annotation-xml></math>"
+      }
+    },
+    {
+      "data": "<math><annotation-xml><svg>x",
+      "errors": [
+        "(1,6): expected-doctype-but-got-start-tag",
+        "(1,28): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "math math": true,
+            "math annotation-xml": true,
+            "svg svg": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "math",
+                    "ns": "http://www.w3.org/1998/Math/MathML",
+                    "children": [
+                      {
+                        "tag": "annotation-xml",
+                        "ns": "http://www.w3.org/1998/Math/MathML",
+                        "children": [
+                          {
+                            "tag": "svg",
+                            "ns": "http://www.w3.org/2000/svg",
+                            "children": [
+                              {
+                                "text": "x"
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><math><annotation-xml><svg>x</svg></annotation-xml></math></body></html>",
+        "noQuirksBodyHtml": "<math><annotation-xml><svg>x</svg></annotation-xml></math>"
+      }
+    }
+  ],
+  "tests21.dat": [
+    {
+      "data": "<svg><![CDATA[foo]]>",
+      "errors": [
+        "(1,5): expected-doctype-but-got-start-tag",
+        "(1,20): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "svg svg": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "svg",
+                    "ns": "http://www.w3.org/2000/svg",
+                    "children": [
+                      {
+                        "text": "foo"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><svg>foo</svg></body></html>",
+        "noQuirksBodyHtml": "<svg>foo</svg>"
+      }
+    },
+    {
+      "data": "<math><![CDATA[foo]]>",
+      "errors": [
+        "(1,6): expected-doctype-but-got-start-tag",
+        "(1,21): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "math math": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "math",
+                    "ns": "http://www.w3.org/1998/Math/MathML",
+                    "children": [
+                      {
+                        "text": "foo"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><math>foo</math></body></html>",
+        "noQuirksBodyHtml": "<math>foo</math>"
+      }
+    },
+    {
+      "data": "<div><![CDATA[foo]]>",
+      "errors": [
+        "(1,5): expected-doctype-but-got-start-tag",
+        "(1,7): expected-dashes-or-doctype",
+        "(1,20): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "div": true
+          },
+          "comment": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "div",
+                    "children": [
+                      {
+                        "comment": "[CDATA[foo]]"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><div><!--[CDATA[foo]]--></div></body></html>",
+        "noQuirksBodyHtml": "<div><!--[CDATA[foo]]--></div>"
+      }
+    },
+    {
+      "data": "<svg><![CDATA[foo",
+      "errors": [
+        "(1,5): expected-doctype-but-got-start-tag",
+        "(1,17): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "svg svg": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "svg",
+                    "ns": "http://www.w3.org/2000/svg",
+                    "children": [
+                      {
+                        "text": "foo"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><svg>foo</svg></body></html>",
+        "noQuirksBodyHtml": "<svg>foo</svg>"
+      }
+    },
+    {
+      "data": "<svg><![CDATA[foo",
+      "errors": [
+        "(1,5): expected-doctype-but-got-start-tag",
+        "(1,17): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "svg svg": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "svg",
+                    "ns": "http://www.w3.org/2000/svg",
+                    "children": [
+                      {
+                        "text": "foo"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><svg>foo</svg></body></html>",
+        "noQuirksBodyHtml": "<svg>foo</svg>"
+      }
+    },
+    {
+      "data": "<svg><![CDATA[",
+      "errors": [
+        "(1,5): expected-doctype-but-got-start-tag",
+        "(1,14): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "svg svg": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "svg",
+                    "ns": "http://www.w3.org/2000/svg"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><svg></svg></body></html>",
+        "noQuirksBodyHtml": "<svg></svg>"
+      }
+    },
+    {
+      "data": "<svg><![CDATA[]]>",
+      "errors": [
+        "(1,5): expected-doctype-but-got-start-tag",
+        "(1,17): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "svg svg": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "svg",
+                    "ns": "http://www.w3.org/2000/svg"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><svg></svg></body></html>",
+        "noQuirksBodyHtml": "<svg></svg>"
+      }
+    },
+    {
+      "data": "<svg><![CDATA[]] >]]>",
+      "errors": [
+        "(1,5): expected-doctype-but-got-start-tag",
+        "(1,21): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "svg svg": true
+          },
+          "escaped": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "svg",
+                    "ns": "http://www.w3.org/2000/svg",
+                    "children": [
+                      {
+                        "text": "]] >",
+                        "escaped": true
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><svg>]] &gt;</svg></body></html>",
+        "noQuirksBodyHtml": "<svg>]] &gt;</svg>"
+      }
+    },
+    {
+      "data": "<svg><![CDATA[]] >]]>",
+      "errors": [
+        "(1,5): expected-doctype-but-got-start-tag",
+        "(1,21): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "svg svg": true
+          },
+          "escaped": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "svg",
+                    "ns": "http://www.w3.org/2000/svg",
+                    "children": [
+                      {
+                        "text": "]] >",
+                        "escaped": true
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><svg>]] &gt;</svg></body></html>",
+        "noQuirksBodyHtml": "<svg>]] &gt;</svg>"
+      }
+    },
+    {
+      "data": "<svg><![CDATA[]]",
+      "errors": [
+        "(1,5): expected-doctype-but-got-start-tag",
+        "(1,16): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "svg svg": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "svg",
+                    "ns": "http://www.w3.org/2000/svg",
+                    "children": [
+                      {
+                        "text": "]]"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><svg>]]</svg></body></html>",
+        "noQuirksBodyHtml": "<svg>]]</svg>"
+      }
+    },
+    {
+      "data": "<svg><![CDATA[]",
+      "errors": [
+        "(1,5): expected-doctype-but-got-start-tag",
+        "(1,15): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "svg svg": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "svg",
+                    "ns": "http://www.w3.org/2000/svg",
+                    "children": [
+                      {
+                        "text": "]"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><svg>]</svg></body></html>",
+        "noQuirksBodyHtml": "<svg>]</svg>"
+      }
+    },
+    {
+      "data": "<svg><![CDATA[]>a",
+      "errors": [
+        "(1,5): expected-doctype-but-got-start-tag",
+        "(1,17): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "svg svg": true
+          },
+          "escaped": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "svg",
+                    "ns": "http://www.w3.org/2000/svg",
+                    "children": [
+                      {
+                        "text": "]>a",
+                        "escaped": true
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><svg>]&gt;a</svg></body></html>",
+        "noQuirksBodyHtml": "<svg>]&gt;a</svg>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><svg><![CDATA[foo]]]>",
+      "errors": [
+        "(1,36): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "svg svg": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "svg",
+                    "ns": "http://www.w3.org/2000/svg",
+                    "children": [
+                      {
+                        "text": "foo]"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><svg>foo]</svg></body></html>",
+        "noQuirksBodyHtml": "<svg>foo]</svg>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><svg><![CDATA[foo]]]]>",
+      "errors": [
+        "(1,37): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "svg svg": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "svg",
+                    "ns": "http://www.w3.org/2000/svg",
+                    "children": [
+                      {
+                        "text": "foo]]"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><svg>foo]]</svg></body></html>",
+        "noQuirksBodyHtml": "<svg>foo]]</svg>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><svg><![CDATA[foo]]]]]>",
+      "errors": [
+        "(1,38): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "svg svg": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "svg",
+                    "ns": "http://www.w3.org/2000/svg",
+                    "children": [
+                      {
+                        "text": "foo]]]"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><svg>foo]]]</svg></body></html>",
+        "noQuirksBodyHtml": "<svg>foo]]]</svg>"
+      }
+    },
+    {
+      "data": "<svg><foreignObject><div><![CDATA[foo]]>",
+      "errors": [
+        "(1,5): expected-doctype-but-got-start-tag",
+        "(1,27): expected-dashes-or-doctype",
+        "(1,40): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "svg svg": true,
+            "svg foreignObject": true,
+            "div": true
+          },
+          "comment": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "svg",
+                    "ns": "http://www.w3.org/2000/svg",
+                    "children": [
+                      {
+                        "tag": "foreignObject",
+                        "ns": "http://www.w3.org/2000/svg",
+                        "children": [
+                          {
+                            "tag": "div",
+                            "children": [
+                              {
+                                "comment": "[CDATA[foo]]"
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><svg><foreignObject><div><!--[CDATA[foo]]--></div></foreignObject></svg></body></html>",
+        "noQuirksBodyHtml": "<svg><foreignObject><div><!--[CDATA[foo]]--></div></foreignObject></svg>"
+      }
+    },
+    {
+      "data": "<svg><![CDATA[<svg>]]>",
+      "errors": [
+        "(1,5): expected-doctype-but-got-start-tag",
+        "(1,22): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "svg svg": true
+          },
+          "escaped": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "svg",
+                    "ns": "http://www.w3.org/2000/svg",
+                    "children": [
+                      {
+                        "text": "<svg>",
+                        "escaped": true
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><svg>&lt;svg&gt;</svg></body></html>",
+        "noQuirksBodyHtml": "<svg>&lt;svg&gt;</svg>"
+      }
+    },
+    {
+      "data": "<svg><![CDATA[</svg>a]]>",
+      "errors": [
+        "(1,5): expected-doctype-but-got-start-tag",
+        "(1,24): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "svg svg": true
+          },
+          "escaped": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "svg",
+                    "ns": "http://www.w3.org/2000/svg",
+                    "children": [
+                      {
+                        "text": "</svg>a",
+                        "escaped": true
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><svg>&lt;/svg&gt;a</svg></body></html>",
+        "noQuirksBodyHtml": "<svg>&lt;/svg&gt;a</svg>"
+      }
+    },
+    {
+      "data": "<svg><![CDATA[<svg>a",
+      "errors": [
+        "(1,5): expected-doctype-but-got-start-tag",
+        "(1,20): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "svg svg": true
+          },
+          "escaped": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "svg",
+                    "ns": "http://www.w3.org/2000/svg",
+                    "children": [
+                      {
+                        "text": "<svg>a",
+                        "escaped": true
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><svg>&lt;svg&gt;a</svg></body></html>",
+        "noQuirksBodyHtml": "<svg>&lt;svg&gt;a</svg>"
+      }
+    },
+    {
+      "data": "<svg><![CDATA[</svg>a",
+      "errors": [
+        "(1,5): expected-doctype-but-got-start-tag",
+        "(1,21): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "svg svg": true
+          },
+          "escaped": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "svg",
+                    "ns": "http://www.w3.org/2000/svg",
+                    "children": [
+                      {
+                        "text": "</svg>a",
+                        "escaped": true
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><svg>&lt;/svg&gt;a</svg></body></html>",
+        "noQuirksBodyHtml": "<svg>&lt;/svg&gt;a</svg>"
+      }
+    },
+    {
+      "data": "<svg><![CDATA[<svg>]]><path>",
+      "errors": [
+        "(1,5): expected-doctype-but-got-start-tag",
+        "(1,28): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "svg svg": true,
+            "svg path": true
+          },
+          "escaped": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "svg",
+                    "ns": "http://www.w3.org/2000/svg",
+                    "children": [
+                      {
+                        "text": "<svg>",
+                        "escaped": true
+                      },
+                      {
+                        "tag": "path",
+                        "ns": "http://www.w3.org/2000/svg"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><svg>&lt;svg&gt;<path></path></svg></body></html>",
+        "noQuirksBodyHtml": "<svg>&lt;svg&gt;<path></path></svg>"
+      }
+    },
+    {
+      "data": "<svg><![CDATA[<svg>]]></path>",
+      "errors": [
+        "(1,5): expected-doctype-but-got-start-tag",
+        "(1,29): unexpected-end-tag",
+        "(1,29): unexpected-end-tag",
+        "(1,29): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "svg svg": true
+          },
+          "escaped": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "svg",
+                    "ns": "http://www.w3.org/2000/svg",
+                    "children": [
+                      {
+                        "text": "<svg>",
+                        "escaped": true
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><svg>&lt;svg&gt;</svg></body></html>",
+        "noQuirksBodyHtml": "<svg>&lt;svg&gt;</svg>"
+      }
+    },
+    {
+      "data": "<svg><![CDATA[<svg>]]><!--path-->",
+      "errors": [
+        "(1,5): expected-doctype-but-got-start-tag",
+        "(1,33): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "svg svg": true
+          },
+          "escaped": true,
+          "comment": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "svg",
+                    "ns": "http://www.w3.org/2000/svg",
+                    "children": [
+                      {
+                        "text": "<svg>",
+                        "escaped": true
+                      },
+                      {
+                        "comment": "path"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><svg>&lt;svg&gt;<!--path--></svg></body></html>",
+        "noQuirksBodyHtml": "<svg>&lt;svg&gt;<!--path--></svg>"
+      }
+    },
+    {
+      "data": "<svg><![CDATA[<svg>]]>path",
+      "errors": [
+        "(1,5): expected-doctype-but-got-start-tag",
+        "(1,26): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "svg svg": true
+          },
+          "escaped": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "svg",
+                    "ns": "http://www.w3.org/2000/svg",
+                    "children": [
+                      {
+                        "text": "<svg>path",
+                        "escaped": true
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><svg>&lt;svg&gt;path</svg></body></html>",
+        "noQuirksBodyHtml": "<svg>&lt;svg&gt;path</svg>"
+      }
+    },
+    {
+      "data": "<svg><![CDATA[<!--svg-->]]>",
+      "errors": [
+        "(1,5): expected-doctype-but-got-start-tag",
+        "(1,27): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "svg svg": true
+          },
+          "escaped": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "svg",
+                    "ns": "http://www.w3.org/2000/svg",
+                    "children": [
+                      {
+                        "text": "<!--svg-->",
+                        "escaped": true
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><svg>&lt;!--svg--&gt;</svg></body></html>",
+        "noQuirksBodyHtml": "<svg>&lt;!--svg--&gt;</svg>"
+      }
+    }
+  ],
+  "tests22.dat": [
+    {
+      "data": "<a><b><big><em><strong><div>X</a>",
+      "errors": [
+        "(1,3): expected-doctype-but-got-start-tag",
+        "(1,33): adoption-agency-1.3",
+        "(1,33): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "a": true,
+            "b": true,
+            "big": true,
+            "em": true,
+            "strong": true,
+            "div": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "a",
+                    "children": [
+                      {
+                        "tag": "b",
+                        "children": [
+                          {
+                            "tag": "big",
+                            "children": [
+                              {
+                                "tag": "em",
+                                "children": [
+                                  {
+                                    "tag": "strong"
+                                  }
+                                ]
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "big",
+                    "children": [
+                      {
+                        "tag": "em",
+                        "children": [
+                          {
+                            "tag": "strong",
+                            "children": [
+                              {
+                                "tag": "div",
+                                "children": [
+                                  {
+                                    "tag": "a",
+                                    "children": [
+                                      {
+                                        "text": "X"
+                                      }
+                                    ]
+                                  }
+                                ]
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><a><b><big><em><strong></strong></em></big></b></a><big><em><strong><div><a>X</a></div></strong></em></big></body></html>",
+        "noQuirksBodyHtml": "<a><b><big><em><strong></strong></em></big></b></a><big><em><strong><div><a>X</a></div></strong></em></big>"
+      }
+    },
+    {
+      "data": "<a><b><div id=1><div id=2><div id=3><div id=4><div id=5><div id=6><div id=7><div id=8>A</a>",
+      "errors": [
+        "(1,3): expected-doctype-but-got-start-tag",
+        "(1,91): adoption-agency-1.3",
+        "(1,91): adoption-agency-1.3",
+        "(1,91): adoption-agency-1.3",
+        "(1,91): adoption-agency-1.3",
+        "(1,91): adoption-agency-1.3",
+        "(1,91): adoption-agency-1.3",
+        "(1,91): adoption-agency-1.3",
+        "(1,91): adoption-agency-1.3",
+        "(1,91): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "a": true,
+            "b": true,
+            "div": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "a",
+                    "children": [
+                      {
+                        "tag": "b"
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "b",
+                    "children": [
+                      {
+                        "tag": "div",
+                        "attrs": [
+                          {
+                            "name": "id",
+                            "value": "1"
+                          }
+                        ],
+                        "children": [
+                          {
+                            "tag": "a"
+                          },
+                          {
+                            "tag": "div",
+                            "attrs": [
+                              {
+                                "name": "id",
+                                "value": "2"
+                              }
+                            ],
+                            "children": [
+                              {
+                                "tag": "a"
+                              },
+                              {
+                                "tag": "div",
+                                "attrs": [
+                                  {
+                                    "name": "id",
+                                    "value": "3"
+                                  }
+                                ],
+                                "children": [
+                                  {
+                                    "tag": "a"
+                                  },
+                                  {
+                                    "tag": "div",
+                                    "attrs": [
+                                      {
+                                        "name": "id",
+                                        "value": "4"
+                                      }
+                                    ],
+                                    "children": [
+                                      {
+                                        "tag": "a"
+                                      },
+                                      {
+                                        "tag": "div",
+                                        "attrs": [
+                                          {
+                                            "name": "id",
+                                            "value": "5"
+                                          }
+                                        ],
+                                        "children": [
+                                          {
+                                            "tag": "a"
+                                          },
+                                          {
+                                            "tag": "div",
+                                            "attrs": [
+                                              {
+                                                "name": "id",
+                                                "value": "6"
+                                              }
+                                            ],
+                                            "children": [
+                                              {
+                                                "tag": "a"
+                                              },
+                                              {
+                                                "tag": "div",
+                                                "attrs": [
+                                                  {
+                                                    "name": "id",
+                                                    "value": "7"
+                                                  }
+                                                ],
+                                                "children": [
+                                                  {
+                                                    "tag": "a"
+                                                  },
+                                                  {
+                                                    "tag": "div",
+                                                    "attrs": [
+                                                      {
+                                                        "name": "id",
+                                                        "value": "8"
+                                                      }
+                                                    ],
+                                                    "children": [
+                                                      {
+                                                        "tag": "a",
+                                                        "children": [
+                                                          {
+                                                            "text": "A"
+                                                          }
+                                                        ]
+                                                      }
+                                                    ]
+                                                  }
+                                                ]
+                                              }
+                                            ]
+                                          }
+                                        ]
+                                      }
+                                    ]
+                                  }
+                                ]
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><a><b></b></a><b><div id=\"1\"><a></a><div id=\"2\"><a></a><div id=\"3\"><a></a><div id=\"4\"><a></a><div id=\"5\"><a></a><div id=\"6\"><a></a><div id=\"7\"><a></a><div id=\"8\"><a>A</a></div></div></div></div></div></div></div></div></b></body></html>",
+        "noQuirksBodyHtml": "<a><b></b></a><b><div id=\"1\"><a></a><div id=\"2\"><a></a><div id=\"3\"><a></a><div id=\"4\"><a></a><div id=\"5\"><a></a><div id=\"6\"><a></a><div id=\"7\"><a></a><div id=\"8\"><a>A</a></div></div></div></div></div></div></div></div></b>"
+      }
+    },
+    {
+      "data": "<a><b><div id=1><div id=2><div id=3><div id=4><div id=5><div id=6><div id=7><div id=8><div id=9>A</a>",
+      "errors": [
+        "(1,3): expected-doctype-but-got-start-tag",
+        "(1,101): adoption-agency-1.3",
+        "(1,101): adoption-agency-1.3",
+        "(1,101): adoption-agency-1.3",
+        "(1,101): adoption-agency-1.3",
+        "(1,101): adoption-agency-1.3",
+        "(1,101): adoption-agency-1.3",
+        "(1,101): adoption-agency-1.3",
+        "(1,101): adoption-agency-1.3",
+        "(1,101): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "a": true,
+            "b": true,
+            "div": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "a",
+                    "children": [
+                      {
+                        "tag": "b"
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "b",
+                    "children": [
+                      {
+                        "tag": "div",
+                        "attrs": [
+                          {
+                            "name": "id",
+                            "value": "1"
+                          }
+                        ],
+                        "children": [
+                          {
+                            "tag": "a"
+                          },
+                          {
+                            "tag": "div",
+                            "attrs": [
+                              {
+                                "name": "id",
+                                "value": "2"
+                              }
+                            ],
+                            "children": [
+                              {
+                                "tag": "a"
+                              },
+                              {
+                                "tag": "div",
+                                "attrs": [
+                                  {
+                                    "name": "id",
+                                    "value": "3"
+                                  }
+                                ],
+                                "children": [
+                                  {
+                                    "tag": "a"
+                                  },
+                                  {
+                                    "tag": "div",
+                                    "attrs": [
+                                      {
+                                        "name": "id",
+                                        "value": "4"
+                                      }
+                                    ],
+                                    "children": [
+                                      {
+                                        "tag": "a"
+                                      },
+                                      {
+                                        "tag": "div",
+                                        "attrs": [
+                                          {
+                                            "name": "id",
+                                            "value": "5"
+                                          }
+                                        ],
+                                        "children": [
+                                          {
+                                            "tag": "a"
+                                          },
+                                          {
+                                            "tag": "div",
+                                            "attrs": [
+                                              {
+                                                "name": "id",
+                                                "value": "6"
+                                              }
+                                            ],
+                                            "children": [
+                                              {
+                                                "tag": "a"
+                                              },
+                                              {
+                                                "tag": "div",
+                                                "attrs": [
+                                                  {
+                                                    "name": "id",
+                                                    "value": "7"
+                                                  }
+                                                ],
+                                                "children": [
+                                                  {
+                                                    "tag": "a"
+                                                  },
+                                                  {
+                                                    "tag": "div",
+                                                    "attrs": [
+                                                      {
+                                                        "name": "id",
+                                                        "value": "8"
+                                                      }
+                                                    ],
+                                                    "children": [
+                                                      {
+                                                        "tag": "a",
+                                                        "children": [
+                                                          {
+                                                            "tag": "div",
+                                                            "attrs": [
+                                                              {
+                                                                "name": "id",
+                                                                "value": "9"
+                                                              }
+                                                            ],
+                                                            "children": [
+                                                              {
+                                                                "text": "A"
+                                                              }
+                                                            ]
+                                                          }
+                                                        ]
+                                                      }
+                                                    ]
+                                                  }
+                                                ]
+                                              }
+                                            ]
+                                          }
+                                        ]
+                                      }
+                                    ]
+                                  }
+                                ]
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><a><b></b></a><b><div id=\"1\"><a></a><div id=\"2\"><a></a><div id=\"3\"><a></a><div id=\"4\"><a></a><div id=\"5\"><a></a><div id=\"6\"><a></a><div id=\"7\"><a></a><div id=\"8\"><a><div id=\"9\">A</div></a></div></div></div></div></div></div></div></div></b></body></html>",
+        "noQuirksBodyHtml": "<a><b></b></a><b><div id=\"1\"><a></a><div id=\"2\"><a></a><div id=\"3\"><a></a><div id=\"4\"><a></a><div id=\"5\"><a></a><div id=\"6\"><a></a><div id=\"7\"><a></a><div id=\"8\"><a><div id=\"9\">A</div></a></div></div></div></div></div></div></div></div></b>"
+      }
+    },
+    {
+      "data": "<a><b><div id=1><div id=2><div id=3><div id=4><div id=5><div id=6><div id=7><div id=8><div id=9><div id=10>A</a>",
+      "errors": [
+        "(1,3): expected-doctype-but-got-start-tag",
+        "(1,112): adoption-agency-1.3",
+        "(1,112): adoption-agency-1.3",
+        "(1,112): adoption-agency-1.3",
+        "(1,112): adoption-agency-1.3",
+        "(1,112): adoption-agency-1.3",
+        "(1,112): adoption-agency-1.3",
+        "(1,112): adoption-agency-1.3",
+        "(1,112): adoption-agency-1.3",
+        "(1,112): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "a": true,
+            "b": true,
+            "div": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "a",
+                    "children": [
+                      {
+                        "tag": "b"
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "b",
+                    "children": [
+                      {
+                        "tag": "div",
+                        "attrs": [
+                          {
+                            "name": "id",
+                            "value": "1"
+                          }
+                        ],
+                        "children": [
+                          {
+                            "tag": "a"
+                          },
+                          {
+                            "tag": "div",
+                            "attrs": [
+                              {
+                                "name": "id",
+                                "value": "2"
+                              }
+                            ],
+                            "children": [
+                              {
+                                "tag": "a"
+                              },
+                              {
+                                "tag": "div",
+                                "attrs": [
+                                  {
+                                    "name": "id",
+                                    "value": "3"
+                                  }
+                                ],
+                                "children": [
+                                  {
+                                    "tag": "a"
+                                  },
+                                  {
+                                    "tag": "div",
+                                    "attrs": [
+                                      {
+                                        "name": "id",
+                                        "value": "4"
+                                      }
+                                    ],
+                                    "children": [
+                                      {
+                                        "tag": "a"
+                                      },
+                                      {
+                                        "tag": "div",
+                                        "attrs": [
+                                          {
+                                            "name": "id",
+                                            "value": "5"
+                                          }
+                                        ],
+                                        "children": [
+                                          {
+                                            "tag": "a"
+                                          },
+                                          {
+                                            "tag": "div",
+                                            "attrs": [
+                                              {
+                                                "name": "id",
+                                                "value": "6"
+                                              }
+                                            ],
+                                            "children": [
+                                              {
+                                                "tag": "a"
+                                              },
+                                              {
+                                                "tag": "div",
+                                                "attrs": [
+                                                  {
+                                                    "name": "id",
+                                                    "value": "7"
+                                                  }
+                                                ],
+                                                "children": [
+                                                  {
+                                                    "tag": "a"
+                                                  },
+                                                  {
+                                                    "tag": "div",
+                                                    "attrs": [
+                                                      {
+                                                        "name": "id",
+                                                        "value": "8"
+                                                      }
+                                                    ],
+                                                    "children": [
+                                                      {
+                                                        "tag": "a",
+                                                        "children": [
+                                                          {
+                                                            "tag": "div",
+                                                            "attrs": [
+                                                              {
+                                                                "name": "id",
+                                                                "value": "9"
+                                                              }
+                                                            ],
+                                                            "children": [
+                                                              {
+                                                                "tag": "div",
+                                                                "attrs": [
+                                                                  {
+                                                                    "name": "id",
+                                                                    "value": "10"
+                                                                  }
+                                                                ],
+                                                                "children": [
+                                                                  {
+                                                                    "text": "A"
+                                                                  }
+                                                                ]
+                                                              }
+                                                            ]
+                                                          }
+                                                        ]
+                                                      }
+                                                    ]
+                                                  }
+                                                ]
+                                              }
+                                            ]
+                                          }
+                                        ]
+                                      }
+                                    ]
+                                  }
+                                ]
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><a><b></b></a><b><div id=\"1\"><a></a><div id=\"2\"><a></a><div id=\"3\"><a></a><div id=\"4\"><a></a><div id=\"5\"><a></a><div id=\"6\"><a></a><div id=\"7\"><a></a><div id=\"8\"><a><div id=\"9\"><div id=\"10\">A</div></div></a></div></div></div></div></div></div></div></div></b></body></html>",
+        "noQuirksBodyHtml": "<a><b></b></a><b><div id=\"1\"><a></a><div id=\"2\"><a></a><div id=\"3\"><a></a><div id=\"4\"><a></a><div id=\"5\"><a></a><div id=\"6\"><a></a><div id=\"7\"><a></a><div id=\"8\"><a><div id=\"9\"><div id=\"10\">A</div></div></a></div></div></div></div></div></div></div></div></b>"
+      }
+    },
+    {
+      "data": "<cite><b><cite><i><cite><i><cite><i><div>X</b>TEST",
+      "errors": [
+        "(1,6): expected-doctype-but-got-start-tag",
+        "(1,46): adoption-agency-1.3",
+        "(1,50): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "cite": true,
+            "b": true,
+            "i": true,
+            "div": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "cite",
+                    "children": [
+                      {
+                        "tag": "b",
+                        "children": [
+                          {
+                            "tag": "cite",
+                            "children": [
+                              {
+                                "tag": "i",
+                                "children": [
+                                  {
+                                    "tag": "cite",
+                                    "children": [
+                                      {
+                                        "tag": "i",
+                                        "children": [
+                                          {
+                                            "tag": "cite",
+                                            "children": [
+                                              {
+                                                "tag": "i"
+                                              }
+                                            ]
+                                          }
+                                        ]
+                                      }
+                                    ]
+                                  }
+                                ]
+                              }
+                            ]
+                          }
+                        ]
+                      },
+                      {
+                        "tag": "i",
+                        "children": [
+                          {
+                            "tag": "i",
+                            "children": [
+                              {
+                                "tag": "div",
+                                "children": [
+                                  {
+                                    "tag": "b",
+                                    "children": [
+                                      {
+                                        "text": "X"
+                                      }
+                                    ]
+                                  },
+                                  {
+                                    "text": "TEST"
+                                  }
+                                ]
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><cite><b><cite><i><cite><i><cite><i></i></cite></i></cite></i></cite></b><i><i><div><b>X</b>TEST</div></i></i></cite></body></html>",
+        "noQuirksBodyHtml": "<cite><b><cite><i><cite><i><cite><i></i></cite></i></cite></i></cite></b><i><i><div><b>X</b>TEST</div></i></i></cite>"
+      }
+    }
+  ],
+  "tests23.dat": [
+    {
+      "data": "<p><font size=4><font color=red><font size=4><font size=4><font size=4><font size=4><font size=4><font color=red><p>X",
+      "errors": [
+        "(1,3): expected-doctype-but-got-start-tag",
+        "(1,116): unexpected-end-tag",
+        "(1,117): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "p": true,
+            "font": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "p",
+                    "children": [
+                      {
+                        "tag": "font",
+                        "attrs": [
+                          {
+                            "name": "size",
+                            "value": "4"
+                          }
+                        ],
+                        "children": [
+                          {
+                            "tag": "font",
+                            "attrs": [
+                              {
+                                "name": "color",
+                                "value": "red"
+                              }
+                            ],
+                            "children": [
+                              {
+                                "tag": "font",
+                                "attrs": [
+                                  {
+                                    "name": "size",
+                                    "value": "4"
+                                  }
+                                ],
+                                "children": [
+                                  {
+                                    "tag": "font",
+                                    "attrs": [
+                                      {
+                                        "name": "size",
+                                        "value": "4"
+                                      }
+                                    ],
+                                    "children": [
+                                      {
+                                        "tag": "font",
+                                        "attrs": [
+                                          {
+                                            "name": "size",
+                                            "value": "4"
+                                          }
+                                        ],
+                                        "children": [
+                                          {
+                                            "tag": "font",
+                                            "attrs": [
+                                              {
+                                                "name": "size",
+                                                "value": "4"
+                                              }
+                                            ],
+                                            "children": [
+                                              {
+                                                "tag": "font",
+                                                "attrs": [
+                                                  {
+                                                    "name": "size",
+                                                    "value": "4"
+                                                  }
+                                                ],
+                                                "children": [
+                                                  {
+                                                    "tag": "font",
+                                                    "attrs": [
+                                                      {
+                                                        "name": "color",
+                                                        "value": "red"
+                                                      }
+                                                    ]
+                                                  }
+                                                ]
+                                              }
+                                            ]
+                                          }
+                                        ]
+                                      }
+                                    ]
+                                  }
+                                ]
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "p",
+                    "children": [
+                      {
+                        "tag": "font",
+                        "attrs": [
+                          {
+                            "name": "color",
+                            "value": "red"
+                          }
+                        ],
+                        "children": [
+                          {
+                            "tag": "font",
+                            "attrs": [
+                              {
+                                "name": "size",
+                                "value": "4"
+                              }
+                            ],
+                            "children": [
+                              {
+                                "tag": "font",
+                                "attrs": [
+                                  {
+                                    "name": "size",
+                                    "value": "4"
+                                  }
+                                ],
+                                "children": [
+                                  {
+                                    "tag": "font",
+                                    "attrs": [
+                                      {
+                                        "name": "size",
+                                        "value": "4"
+                                      }
+                                    ],
+                                    "children": [
+                                      {
+                                        "tag": "font",
+                                        "attrs": [
+                                          {
+                                            "name": "color",
+                                            "value": "red"
+                                          }
+                                        ],
+                                        "children": [
+                                          {
+                                            "text": "X"
+                                          }
+                                        ]
+                                      }
+                                    ]
+                                  }
+                                ]
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><p><font size=\"4\"><font color=\"red\"><font size=\"4\"><font size=\"4\"><font size=\"4\"><font size=\"4\"><font size=\"4\"><font color=\"red\"></font></font></font></font></font></font></font></font></p><p><font color=\"red\"><font size=\"4\"><font size=\"4\"><font size=\"4\"><font color=\"red\">X</font></font></font></font></font></p></body></html>",
+        "noQuirksBodyHtml": "<p><font size=\"4\"><font color=\"red\"><font size=\"4\"><font size=\"4\"><font size=\"4\"><font size=\"4\"><font size=\"4\"><font color=\"red\"></font></font></font></font></font></font></font></font></p><p><font color=\"red\"><font size=\"4\"><font size=\"4\"><font size=\"4\"><font color=\"red\">X</font></font></font></font></font></p>"
+      }
+    },
+    {
+      "data": "<p><font size=4><font size=4><font size=4><font size=4><p>X",
+      "errors": [
+        "(1,3): expected-doctype-but-got-start-tag",
+        "(1,58): unexpected-end-tag",
+        "(1,59): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "p": true,
+            "font": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "p",
+                    "children": [
+                      {
+                        "tag": "font",
+                        "attrs": [
+                          {
+                            "name": "size",
+                            "value": "4"
+                          }
+                        ],
+                        "children": [
+                          {
+                            "tag": "font",
+                            "attrs": [
+                              {
+                                "name": "size",
+                                "value": "4"
+                              }
+                            ],
+                            "children": [
+                              {
+                                "tag": "font",
+                                "attrs": [
+                                  {
+                                    "name": "size",
+                                    "value": "4"
+                                  }
+                                ],
+                                "children": [
+                                  {
+                                    "tag": "font",
+                                    "attrs": [
+                                      {
+                                        "name": "size",
+                                        "value": "4"
+                                      }
+                                    ]
+                                  }
+                                ]
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "p",
+                    "children": [
+                      {
+                        "tag": "font",
+                        "attrs": [
+                          {
+                            "name": "size",
+                            "value": "4"
+                          }
+                        ],
+                        "children": [
+                          {
+                            "tag": "font",
+                            "attrs": [
+                              {
+                                "name": "size",
+                                "value": "4"
+                              }
+                            ],
+                            "children": [
+                              {
+                                "tag": "font",
+                                "attrs": [
+                                  {
+                                    "name": "size",
+                                    "value": "4"
+                                  }
+                                ],
+                                "children": [
+                                  {
+                                    "text": "X"
+                                  }
+                                ]
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><p><font size=\"4\"><font size=\"4\"><font size=\"4\"><font size=\"4\"></font></font></font></font></p><p><font size=\"4\"><font size=\"4\"><font size=\"4\">X</font></font></font></p></body></html>",
+        "noQuirksBodyHtml": "<p><font size=\"4\"><font size=\"4\"><font size=\"4\"><font size=\"4\"></font></font></font></font></p><p><font size=\"4\"><font size=\"4\"><font size=\"4\">X</font></font></font></p>"
+      }
+    },
+    {
+      "data": "<p><font size=4><font size=4><font size=4><font size=\"5\"><font size=4><p>X",
+      "errors": [
+        "(1,3): expected-doctype-but-got-start-tag",
+        "(1,73): unexpected-end-tag",
+        "(1,74): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "p": true,
+            "font": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "p",
+                    "children": [
+                      {
+                        "tag": "font",
+                        "attrs": [
+                          {
+                            "name": "size",
+                            "value": "4"
+                          }
+                        ],
+                        "children": [
+                          {
+                            "tag": "font",
+                            "attrs": [
+                              {
+                                "name": "size",
+                                "value": "4"
+                              }
+                            ],
+                            "children": [
+                              {
+                                "tag": "font",
+                                "attrs": [
+                                  {
+                                    "name": "size",
+                                    "value": "4"
+                                  }
+                                ],
+                                "children": [
+                                  {
+                                    "tag": "font",
+                                    "attrs": [
+                                      {
+                                        "name": "size",
+                                        "value": "5"
+                                      }
+                                    ],
+                                    "children": [
+                                      {
+                                        "tag": "font",
+                                        "attrs": [
+                                          {
+                                            "name": "size",
+                                            "value": "4"
+                                          }
+                                        ]
+                                      }
+                                    ]
+                                  }
+                                ]
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "p",
+                    "children": [
+                      {
+                        "tag": "font",
+                        "attrs": [
+                          {
+                            "name": "size",
+                            "value": "4"
+                          }
+                        ],
+                        "children": [
+                          {
+                            "tag": "font",
+                            "attrs": [
+                              {
+                                "name": "size",
+                                "value": "4"
+                              }
+                            ],
+                            "children": [
+                              {
+                                "tag": "font",
+                                "attrs": [
+                                  {
+                                    "name": "size",
+                                    "value": "5"
+                                  }
+                                ],
+                                "children": [
+                                  {
+                                    "tag": "font",
+                                    "attrs": [
+                                      {
+                                        "name": "size",
+                                        "value": "4"
+                                      }
+                                    ],
+                                    "children": [
+                                      {
+                                        "text": "X"
+                                      }
+                                    ]
+                                  }
+                                ]
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><p><font size=\"4\"><font size=\"4\"><font size=\"4\"><font size=\"5\"><font size=\"4\"></font></font></font></font></font></p><p><font size=\"4\"><font size=\"4\"><font size=\"5\"><font size=\"4\">X</font></font></font></font></p></body></html>",
+        "noQuirksBodyHtml": "<p><font size=\"4\"><font size=\"4\"><font size=\"4\"><font size=\"5\"><font size=\"4\"></font></font></font></font></font></p><p><font size=\"4\"><font size=\"4\"><font size=\"5\"><font size=\"4\">X</font></font></font></font></p>"
+      }
+    },
+    {
+      "data": "<p><font size=4 id=a><font size=4 id=b><font size=4><font size=4><p>X",
+      "errors": [
+        "(1,3): expected-doctype-but-got-start-tag",
+        "(1,68): unexpected-end-tag",
+        "(1,69): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "p": true,
+            "font": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "p",
+                    "children": [
+                      {
+                        "tag": "font",
+                        "attrs": [
+                          {
+                            "name": "id",
+                            "value": "a"
+                          },
+                          {
+                            "name": "size",
+                            "value": "4"
+                          }
+                        ],
+                        "children": [
+                          {
+                            "tag": "font",
+                            "attrs": [
+                              {
+                                "name": "id",
+                                "value": "b"
+                              },
+                              {
+                                "name": "size",
+                                "value": "4"
+                              }
+                            ],
+                            "children": [
+                              {
+                                "tag": "font",
+                                "attrs": [
+                                  {
+                                    "name": "size",
+                                    "value": "4"
+                                  }
+                                ],
+                                "children": [
+                                  {
+                                    "tag": "font",
+                                    "attrs": [
+                                      {
+                                        "name": "size",
+                                        "value": "4"
+                                      }
+                                    ]
+                                  }
+                                ]
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "p",
+                    "children": [
+                      {
+                        "tag": "font",
+                        "attrs": [
+                          {
+                            "name": "id",
+                            "value": "a"
+                          },
+                          {
+                            "name": "size",
+                            "value": "4"
+                          }
+                        ],
+                        "children": [
+                          {
+                            "tag": "font",
+                            "attrs": [
+                              {
+                                "name": "id",
+                                "value": "b"
+                              },
+                              {
+                                "name": "size",
+                                "value": "4"
+                              }
+                            ],
+                            "children": [
+                              {
+                                "tag": "font",
+                                "attrs": [
+                                  {
+                                    "name": "size",
+                                    "value": "4"
+                                  }
+                                ],
+                                "children": [
+                                  {
+                                    "tag": "font",
+                                    "attrs": [
+                                      {
+                                        "name": "size",
+                                        "value": "4"
+                                      }
+                                    ],
+                                    "children": [
+                                      {
+                                        "text": "X"
+                                      }
+                                    ]
+                                  }
+                                ]
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><p><font size=\"4\" id=\"a\"><font size=\"4\" id=\"b\"><font size=\"4\"><font size=\"4\"></font></font></font></font></p><p><font size=\"4\" id=\"a\"><font size=\"4\" id=\"b\"><font size=\"4\"><font size=\"4\">X</font></font></font></font></p></body></html>",
+        "noQuirksBodyHtml": "<p><font size=\"4\" id=\"a\"><font size=\"4\" id=\"b\"><font size=\"4\"><font size=\"4\"></font></font></font></font></p><p><font size=\"4\" id=\"a\"><font size=\"4\" id=\"b\"><font size=\"4\"><font size=\"4\">X</font></font></font></font></p>"
+      }
+    },
+    {
+      "data": "<p><b id=a><b id=a><b id=a><b><object><b id=a><b id=a>X</object><p>Y",
+      "errors": [
+        "(1,3): expected-doctype-but-got-start-tag",
+        "(1,64): end-tag-too-early",
+        "(1,67): unexpected-end-tag",
+        "(1,68): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "p": true,
+            "b": true,
+            "object": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "p",
+                    "children": [
+                      {
+                        "tag": "b",
+                        "attrs": [
+                          {
+                            "name": "id",
+                            "value": "a"
+                          }
+                        ],
+                        "children": [
+                          {
+                            "tag": "b",
+                            "attrs": [
+                              {
+                                "name": "id",
+                                "value": "a"
+                              }
+                            ],
+                            "children": [
+                              {
+                                "tag": "b",
+                                "attrs": [
+                                  {
+                                    "name": "id",
+                                    "value": "a"
+                                  }
+                                ],
+                                "children": [
+                                  {
+                                    "tag": "b",
+                                    "children": [
+                                      {
+                                        "tag": "object",
+                                        "children": [
+                                          {
+                                            "tag": "b",
+                                            "attrs": [
+                                              {
+                                                "name": "id",
+                                                "value": "a"
+                                              }
+                                            ],
+                                            "children": [
+                                              {
+                                                "tag": "b",
+                                                "attrs": [
+                                                  {
+                                                    "name": "id",
+                                                    "value": "a"
+                                                  }
+                                                ],
+                                                "children": [
+                                                  {
+                                                    "text": "X"
+                                                  }
+                                                ]
+                                              }
+                                            ]
+                                          }
+                                        ]
+                                      }
+                                    ]
+                                  }
+                                ]
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "p",
+                    "children": [
+                      {
+                        "tag": "b",
+                        "attrs": [
+                          {
+                            "name": "id",
+                            "value": "a"
+                          }
+                        ],
+                        "children": [
+                          {
+                            "tag": "b",
+                            "attrs": [
+                              {
+                                "name": "id",
+                                "value": "a"
+                              }
+                            ],
+                            "children": [
+                              {
+                                "tag": "b",
+                                "attrs": [
+                                  {
+                                    "name": "id",
+                                    "value": "a"
+                                  }
+                                ],
+                                "children": [
+                                  {
+                                    "tag": "b",
+                                    "children": [
+                                      {
+                                        "text": "Y"
+                                      }
+                                    ]
+                                  }
+                                ]
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><p><b id=\"a\"><b id=\"a\"><b id=\"a\"><b><object><b id=\"a\"><b id=\"a\">X</b></b></object></b></b></b></b></p><p><b id=\"a\"><b id=\"a\"><b id=\"a\"><b>Y</b></b></b></b></p></body></html>",
+        "noQuirksBodyHtml": "<p><b id=\"a\"><b id=\"a\"><b id=\"a\"><b><object><b id=\"a\"><b id=\"a\">X</b></b></object></b></b></b></b></p><p><b id=\"a\"><b id=\"a\"><b id=\"a\"><b>Y</b></b></b></b></p>"
+      }
+    }
+  ],
+  "tests24.dat": [
+    {
+      "data": "<!DOCTYPE html>&NotEqualTilde;",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "≂̸"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body>≂̸</body></html>",
+        "noQuirksBodyHtml": "≂̸"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html>&NotEqualTilde;A",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "≂̸A"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body>≂̸A</body></html>",
+        "noQuirksBodyHtml": "≂̸A"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html>&ThickSpace;",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "  "
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body>  </body></html>",
+        "noQuirksBodyHtml": "  "
+      }
+    },
+    {
+      "data": "<!DOCTYPE html>&ThickSpace;A",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "  A"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body>  A</body></html>",
+        "noQuirksBodyHtml": "  A"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html>&NotSubset;",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "⊂⃒"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body>⊂⃒</body></html>",
+        "noQuirksBodyHtml": "⊂⃒"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html>&NotSubset;A",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "⊂⃒A"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body>⊂⃒A</body></html>",
+        "noQuirksBodyHtml": "⊂⃒A"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html>&Gopf;",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "𝔾"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body>𝔾</body></html>",
+        "noQuirksBodyHtml": "𝔾"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html>&Gopf;A",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "𝔾A"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body>𝔾A</body></html>",
+        "noQuirksBodyHtml": "𝔾A"
+      }
+    }
+  ],
+  "tests25.dat": [
+    {
+      "data": "<!DOCTYPE html><body><foo>A",
+      "errors": [
+        "(1,27): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "foo": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "foo",
+                    "children": [
+                      {
+                        "text": "A"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><foo>A</foo></body></html>",
+        "noQuirksBodyHtml": "<foo>A</foo>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><body><area>A",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "area": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "area"
+                  },
+                  {
+                    "text": "A"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><area>A</body></html>",
+        "noQuirksBodyHtml": "<area>A"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><body><base>A",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "base": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "base"
+                  },
+                  {
+                    "text": "A"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><base>A</body></html>",
+        "noQuirksBodyHtml": "<base>A"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><body><basefont>A",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "basefont": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "basefont"
+                  },
+                  {
+                    "text": "A"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><basefont>A</body></html>",
+        "noQuirksBodyHtml": "<basefont>A"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><body><bgsound>A",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "bgsound": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "bgsound"
+                  },
+                  {
+                    "text": "A"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><bgsound>A</body></html>",
+        "noQuirksBodyHtml": "<bgsound>A"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><body><br>A",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "br": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "br"
+                  },
+                  {
+                    "text": "A"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><br>A</body></html>",
+        "noQuirksBodyHtml": "<br>A"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><body><col>A",
+      "errors": [
+        "(1,26): unexpected-start-tag-ignored"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "A"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body>A</body></html>",
+        "noQuirksBodyHtml": "A"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><body><command>A",
+      "errors": [
+        "eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "command": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "command",
+                    "children": [
+                      {
+                        "text": "A"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><command>A</command></body></html>",
+        "noQuirksBodyHtml": "<command>A</command>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><body><embed>A",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "embed": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "embed"
+                  },
+                  {
+                    "text": "A"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><embed>A</body></html>",
+        "noQuirksBodyHtml": "<embed>A"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><body><frame>A",
+      "errors": [
+        "(1,28): unexpected-start-tag-ignored"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "A"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body>A</body></html>",
+        "noQuirksBodyHtml": "A"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><body><hr>A",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "hr": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "hr"
+                  },
+                  {
+                    "text": "A"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><hr>A</body></html>",
+        "noQuirksBodyHtml": "<hr>A"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><body><img>A",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "img": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "img"
+                  },
+                  {
+                    "text": "A"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><img>A</body></html>",
+        "noQuirksBodyHtml": "<img>A"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><body><input>A",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "input": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "input"
+                  },
+                  {
+                    "text": "A"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><input>A</body></html>",
+        "noQuirksBodyHtml": "<input>A"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><body><keygen>A",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "keygen": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "keygen"
+                  },
+                  {
+                    "text": "A"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><keygen>A</body></html>",
+        "noQuirksBodyHtml": "<keygen>A"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><body><link>A",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "link": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "link"
+                  },
+                  {
+                    "text": "A"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><link>A</body></html>",
+        "noQuirksBodyHtml": "<link>A"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><body><meta>A",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "meta": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "meta"
+                  },
+                  {
+                    "text": "A"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><meta>A</body></html>",
+        "noQuirksBodyHtml": "<meta>A"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><body><param>A",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "param": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "param"
+                  },
+                  {
+                    "text": "A"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><param>A</body></html>",
+        "noQuirksBodyHtml": "<param>A"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><body><source>A",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "source": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "source"
+                  },
+                  {
+                    "text": "A"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><source>A</body></html>",
+        "noQuirksBodyHtml": "<source>A"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><body><track>A",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "track": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "track"
+                  },
+                  {
+                    "text": "A"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><track>A</body></html>",
+        "noQuirksBodyHtml": "<track>A"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><body><wbr>A",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "wbr": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "wbr"
+                  },
+                  {
+                    "text": "A"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><wbr>A</body></html>",
+        "noQuirksBodyHtml": "<wbr>A"
+      }
+    }
+  ],
+  "tests26.dat": [
+    {
+      "data": "<!DOCTYPE html><body><a href='#1'><nobr>1<nobr></a><br><a href='#2'><nobr>2<nobr></a><br><a href='#3'><nobr>3<nobr></a>",
+      "errors": [
+        "(1,47): unexpected-start-tag-implies-end-tag",
+        "(1,51): adoption-agency-1.3",
+        "(1,74): unexpected-start-tag-implies-end-tag",
+        "(1,74): adoption-agency-1.3",
+        "(1,81): unexpected-start-tag-implies-end-tag",
+        "(1,85): adoption-agency-1.3",
+        "(1,108): unexpected-start-tag-implies-end-tag",
+        "(1,108): adoption-agency-1.3",
+        "(1,115): unexpected-start-tag-implies-end-tag",
+        "(1,119): adoption-agency-1.3"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "a": true,
+            "nobr": true,
+            "br": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "a",
+                    "attrs": [
+                      {
+                        "name": "href",
+                        "value": "#1"
+                      }
+                    ],
+                    "children": [
+                      {
+                        "tag": "nobr",
+                        "children": [
+                          {
+                            "text": "1"
+                          }
+                        ]
+                      },
+                      {
+                        "tag": "nobr"
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "nobr",
+                    "children": [
+                      {
+                        "tag": "br"
+                      },
+                      {
+                        "tag": "a",
+                        "attrs": [
+                          {
+                            "name": "href",
+                            "value": "#2"
+                          }
+                        ]
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "a",
+                    "attrs": [
+                      {
+                        "name": "href",
+                        "value": "#2"
+                      }
+                    ],
+                    "children": [
+                      {
+                        "tag": "nobr",
+                        "children": [
+                          {
+                            "text": "2"
+                          }
+                        ]
+                      },
+                      {
+                        "tag": "nobr"
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "nobr",
+                    "children": [
+                      {
+                        "tag": "br"
+                      },
+                      {
+                        "tag": "a",
+                        "attrs": [
+                          {
+                            "name": "href",
+                            "value": "#3"
+                          }
+                        ]
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "a",
+                    "attrs": [
+                      {
+                        "name": "href",
+                        "value": "#3"
+                      }
+                    ],
+                    "children": [
+                      {
+                        "tag": "nobr",
+                        "children": [
+                          {
+                            "text": "3"
+                          }
+                        ]
+                      },
+                      {
+                        "tag": "nobr"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><a href=\"#1\"><nobr>1</nobr><nobr></nobr></a><nobr><br><a href=\"#2\"></a></nobr><a href=\"#2\"><nobr>2</nobr><nobr></nobr></a><nobr><br><a href=\"#3\"></a></nobr><a href=\"#3\"><nobr>3</nobr><nobr></nobr></a></body></html>",
+        "noQuirksBodyHtml": "<a href=\"#1\"><nobr>1</nobr><nobr></nobr></a><nobr><br><a href=\"#2\"></a></nobr><a href=\"#2\"><nobr>2</nobr><nobr></nobr></a><nobr><br><a href=\"#3\"></a></nobr><a href=\"#3\"><nobr>3</nobr><nobr></nobr></a>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><body><b><nobr>1<nobr></b><i><nobr>2<nobr></i>3",
+      "errors": [
+        "(1,37): unexpected-start-tag-implies-end-tag",
+        "(1,41): adoption-agency-1.3",
+        "(1,50): unexpected-start-tag-implies-end-tag",
+        "(1,50): adoption-agency-1.3",
+        "(1,57): unexpected-start-tag-implies-end-tag",
+        "(1,61): adoption-agency-1.3",
+        "(1,62): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "b": true,
+            "nobr": true,
+            "i": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "b",
+                    "children": [
+                      {
+                        "tag": "nobr",
+                        "children": [
+                          {
+                            "text": "1"
+                          }
+                        ]
+                      },
+                      {
+                        "tag": "nobr"
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "nobr",
+                    "children": [
+                      {
+                        "tag": "i"
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "i",
+                    "children": [
+                      {
+                        "tag": "nobr",
+                        "children": [
+                          {
+                            "text": "2"
+                          }
+                        ]
+                      },
+                      {
+                        "tag": "nobr"
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "nobr",
+                    "children": [
+                      {
+                        "text": "3"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><b><nobr>1</nobr><nobr></nobr></b><nobr><i></i></nobr><i><nobr>2</nobr><nobr></nobr></i><nobr>3</nobr></body></html>",
+        "noQuirksBodyHtml": "<b><nobr>1</nobr><nobr></nobr></b><nobr><i></i></nobr><i><nobr>2</nobr><nobr></nobr></i><nobr>3</nobr>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><body><b><nobr>1<table><nobr></b><i><nobr>2<nobr></i>3",
+      "errors": [
+        "(1,44): foster-parenting-start-tag",
+        "(1,48): foster-parenting-end-tag",
+        "(1,48): adoption-agency-1.3",
+        "(1,51): foster-parenting-start-tag",
+        "(1,57): foster-parenting-start-tag",
+        "(1,57): nobr-already-in-scope",
+        "(1,57): adoption-agency-1.2",
+        "(1,58): foster-parenting-character",
+        "(1,64): foster-parenting-start-tag",
+        "(1,64): nobr-already-in-scope",
+        "(1,68): foster-parenting-end-tag",
+        "(1,68): adoption-agency-1.2",
+        "(1,69): foster-parenting-character",
+        "(1,69): eof-in-table"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "b": true,
+            "nobr": true,
+            "i": true,
+            "table": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "b",
+                    "children": [
+                      {
+                        "tag": "nobr",
+                        "children": [
+                          {
+                            "text": "1"
+                          },
+                          {
+                            "tag": "nobr",
+                            "children": [
+                              {
+                                "tag": "i"
+                              }
+                            ]
+                          },
+                          {
+                            "tag": "i",
+                            "children": [
+                              {
+                                "tag": "nobr",
+                                "children": [
+                                  {
+                                    "text": "2"
+                                  }
+                                ]
+                              },
+                              {
+                                "tag": "nobr"
+                              }
+                            ]
+                          },
+                          {
+                            "tag": "nobr",
+                            "children": [
+                              {
+                                "text": "3"
+                              }
+                            ]
+                          },
+                          {
+                            "tag": "table"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><b><nobr>1<nobr><i></i></nobr><i><nobr>2</nobr><nobr></nobr></i><nobr>3</nobr><table></table></nobr></b></body></html>",
+        "noQuirksBodyHtml": "<b><nobr>1<nobr><i></i></nobr><i><nobr>2</nobr><nobr></nobr></i><nobr>3</nobr><table></table></nobr></b>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><body><b><nobr>1<table><tr><td><nobr></b><i><nobr>2<nobr></i>3",
+      "errors": [
+        "(1,56): unexpected-end-tag",
+        "(1,65): unexpected-start-tag-implies-end-tag",
+        "(1,65): adoption-agency-1.3",
+        "(1,72): unexpected-start-tag-implies-end-tag",
+        "(1,76): adoption-agency-1.3",
+        "(1,77): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "b": true,
+            "nobr": true,
+            "table": true,
+            "tbody": true,
+            "tr": true,
+            "td": true,
+            "i": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "b",
+                    "children": [
+                      {
+                        "tag": "nobr",
+                        "children": [
+                          {
+                            "text": "1"
+                          },
+                          {
+                            "tag": "table",
+                            "children": [
+                              {
+                                "tag": "tbody",
+                                "children": [
+                                  {
+                                    "tag": "tr",
+                                    "children": [
+                                      {
+                                        "tag": "td",
+                                        "children": [
+                                          {
+                                            "tag": "nobr",
+                                            "children": [
+                                              {
+                                                "tag": "i"
+                                              }
+                                            ]
+                                          },
+                                          {
+                                            "tag": "i",
+                                            "children": [
+                                              {
+                                                "tag": "nobr",
+                                                "children": [
+                                                  {
+                                                    "text": "2"
+                                                  }
+                                                ]
+                                              },
+                                              {
+                                                "tag": "nobr"
+                                              }
+                                            ]
+                                          },
+                                          {
+                                            "tag": "nobr",
+                                            "children": [
+                                              {
+                                                "text": "3"
+                                              }
+                                            ]
+                                          }
+                                        ]
+                                      }
+                                    ]
+                                  }
+                                ]
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><b><nobr>1<table><tbody><tr><td><nobr><i></i></nobr><i><nobr>2</nobr><nobr></nobr></i><nobr>3</nobr></td></tr></tbody></table></nobr></b></body></html>",
+        "noQuirksBodyHtml": "<b><nobr>1<table><tbody><tr><td><nobr><i></i></nobr><i><nobr>2</nobr><nobr></nobr></i><nobr>3</nobr></td></tr></tbody></table></nobr></b>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><body><b><nobr>1<div><nobr></b><i><nobr>2<nobr></i>3",
+      "errors": [
+        "(1,42): unexpected-start-tag-implies-end-tag",
+        "(1,42): adoption-agency-1.3",
+        "(1,46): adoption-agency-1.3",
+        "(1,46): adoption-agency-1.3",
+        "(1,55): unexpected-start-tag-implies-end-tag",
+        "(1,55): adoption-agency-1.3",
+        "(1,62): unexpected-start-tag-implies-end-tag",
+        "(1,66): adoption-agency-1.3",
+        "(1,67): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "b": true,
+            "nobr": true,
+            "div": true,
+            "i": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "b",
+                    "children": [
+                      {
+                        "tag": "nobr",
+                        "children": [
+                          {
+                            "text": "1"
+                          }
+                        ]
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "div",
+                    "children": [
+                      {
+                        "tag": "b",
+                        "children": [
+                          {
+                            "tag": "nobr"
+                          },
+                          {
+                            "tag": "nobr"
+                          }
+                        ]
+                      },
+                      {
+                        "tag": "nobr",
+                        "children": [
+                          {
+                            "tag": "i"
+                          }
+                        ]
+                      },
+                      {
+                        "tag": "i",
+                        "children": [
+                          {
+                            "tag": "nobr",
+                            "children": [
+                              {
+                                "text": "2"
+                              }
+                            ]
+                          },
+                          {
+                            "tag": "nobr"
+                          }
+                        ]
+                      },
+                      {
+                        "tag": "nobr",
+                        "children": [
+                          {
+                            "text": "3"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><b><nobr>1</nobr></b><div><b><nobr></nobr><nobr></nobr></b><nobr><i></i></nobr><i><nobr>2</nobr><nobr></nobr></i><nobr>3</nobr></div></body></html>",
+        "noQuirksBodyHtml": "<b><nobr>1</nobr></b><div><b><nobr></nobr><nobr></nobr></b><nobr><i></i></nobr><i><nobr>2</nobr><nobr></nobr></i><nobr>3</nobr></div>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><body><b><nobr>1<nobr></b><div><i><nobr>2<nobr></i>3",
+      "errors": [
+        "(1,37): unexpected-start-tag-implies-end-tag",
+        "(1,41): adoption-agency-1.3",
+        "(1,55): unexpected-start-tag-implies-end-tag",
+        "(1,55): adoption-agency-1.3",
+        "(1,62): unexpected-start-tag-implies-end-tag",
+        "(1,66): adoption-agency-1.3",
+        "(1,67): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "b": true,
+            "nobr": true,
+            "div": true,
+            "i": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "b",
+                    "children": [
+                      {
+                        "tag": "nobr",
+                        "children": [
+                          {
+                            "text": "1"
+                          }
+                        ]
+                      },
+                      {
+                        "tag": "nobr"
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "div",
+                    "children": [
+                      {
+                        "tag": "nobr",
+                        "children": [
+                          {
+                            "tag": "i"
+                          }
+                        ]
+                      },
+                      {
+                        "tag": "i",
+                        "children": [
+                          {
+                            "tag": "nobr",
+                            "children": [
+                              {
+                                "text": "2"
+                              }
+                            ]
+                          },
+                          {
+                            "tag": "nobr"
+                          }
+                        ]
+                      },
+                      {
+                        "tag": "nobr",
+                        "children": [
+                          {
+                            "text": "3"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><b><nobr>1</nobr><nobr></nobr></b><div><nobr><i></i></nobr><i><nobr>2</nobr><nobr></nobr></i><nobr>3</nobr></div></body></html>",
+        "noQuirksBodyHtml": "<b><nobr>1</nobr><nobr></nobr></b><div><nobr><i></i></nobr><i><nobr>2</nobr><nobr></nobr></i><nobr>3</nobr></div>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><body><b><nobr>1<nobr><ins></b><i><nobr>",
+      "errors": [
+        "(1,37): unexpected-start-tag-implies-end-tag",
+        "(1,46): adoption-agency-1.3",
+        "(1,55): unexpected-start-tag-implies-end-tag",
+        "(1,55): adoption-agency-1.3",
+        "(1,55): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "b": true,
+            "nobr": true,
+            "ins": true,
+            "i": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "b",
+                    "children": [
+                      {
+                        "tag": "nobr",
+                        "children": [
+                          {
+                            "text": "1"
+                          }
+                        ]
+                      },
+                      {
+                        "tag": "nobr",
+                        "children": [
+                          {
+                            "tag": "ins"
+                          }
+                        ]
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "nobr",
+                    "children": [
+                      {
+                        "tag": "i"
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "i",
+                    "children": [
+                      {
+                        "tag": "nobr"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><b><nobr>1</nobr><nobr><ins></ins></nobr></b><nobr><i></i></nobr><i><nobr></nobr></i></body></html>",
+        "noQuirksBodyHtml": "<b><nobr>1</nobr><nobr><ins></ins></nobr></b><nobr><i></i></nobr><i><nobr></nobr></i>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><body><b><nobr>1<ins><nobr></b><i>2",
+      "errors": [
+        "(1,42): unexpected-start-tag-implies-end-tag",
+        "(1,42): adoption-agency-1.3",
+        "(1,46): adoption-agency-1.3",
+        "(1,50): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "b": true,
+            "nobr": true,
+            "ins": true,
+            "i": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "b",
+                    "children": [
+                      {
+                        "tag": "nobr",
+                        "children": [
+                          {
+                            "text": "1"
+                          },
+                          {
+                            "tag": "ins"
+                          }
+                        ]
+                      },
+                      {
+                        "tag": "nobr"
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "nobr",
+                    "children": [
+                      {
+                        "tag": "i",
+                        "children": [
+                          {
+                            "text": "2"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><b><nobr>1<ins></ins></nobr><nobr></nobr></b><nobr><i>2</i></nobr></body></html>",
+        "noQuirksBodyHtml": "<b><nobr>1<ins></ins></nobr><nobr></nobr></b><nobr><i>2</i></nobr>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><body><b>1<nobr></b><i><nobr>2</i>",
+      "errors": [
+        "(1,35): adoption-agency-1.3",
+        "(1,44): unexpected-start-tag-implies-end-tag",
+        "(1,44): adoption-agency-1.3",
+        "(1,49): adoption-agency-1.3"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "b": true,
+            "nobr": true,
+            "i": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "b",
+                    "children": [
+                      {
+                        "text": "1"
+                      },
+                      {
+                        "tag": "nobr"
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "nobr",
+                    "children": [
+                      {
+                        "tag": "i"
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "i",
+                    "children": [
+                      {
+                        "tag": "nobr",
+                        "children": [
+                          {
+                            "text": "2"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><b>1<nobr></nobr></b><nobr><i></i></nobr><i><nobr>2</nobr></i></body></html>",
+        "noQuirksBodyHtml": "<b>1<nobr></nobr></b><nobr><i></i></nobr><i><nobr>2</nobr></i>"
+      }
+    },
+    {
+      "data": "<p><code x</code></p>\n",
+      "errors": [
+        "(1,3): expected-doctype-but-got-start-tag",
+        "(1,11): invalid-character-in-attribute-name",
+        "(1,12): unexpected-character-after-solidus-in-tag",
+        "(1,21): unexpected-end-tag",
+        "(2,0): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "p": true,
+            "code": true
+          },
+          "attrWithFunnyChar": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "p",
+                    "children": [
+                      {
+                        "tag": "code",
+                        "attrs": [
+                          {
+                            "name": "code",
+                            "value": ""
+                          },
+                          {
+                            "name": "x<",
+                            "value": ""
+                          }
+                        ]
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "code",
+                    "attrs": [
+                      {
+                        "name": "code",
+                        "value": ""
+                      },
+                      {
+                        "name": "x<",
+                        "value": ""
+                      }
+                    ],
+                    "children": [
+                      {
+                        "text": "\n"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><p><code x<=\"\" code=\"\"></code></p><code x<=\"\" code=\"\">\n</code></body></html>",
+        "noQuirksBodyHtml": "<p><code x<=\"\" code=\"\"></code></p><code x<=\"\" code=\"\">\n</code>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><svg><foreignObject><p><i></p>a",
+      "errors": [
+        "(1,45): unexpected-end-tag",
+        "(1,46): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "svg svg": true,
+            "svg foreignObject": true,
+            "p": true,
+            "i": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "svg",
+                    "ns": "http://www.w3.org/2000/svg",
+                    "children": [
+                      {
+                        "tag": "foreignObject",
+                        "ns": "http://www.w3.org/2000/svg",
+                        "children": [
+                          {
+                            "tag": "p",
+                            "children": [
+                              {
+                                "tag": "i"
+                              }
+                            ]
+                          },
+                          {
+                            "tag": "i",
+                            "children": [
+                              {
+                                "text": "a"
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><svg><foreignObject><p><i></i></p><i>a</i></foreignObject></svg></body></html>",
+        "noQuirksBodyHtml": "<svg><foreignObject><p><i></i></p><i>a</i></foreignObject></svg>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><table><tr><td><svg><foreignObject><p><i></p>a",
+      "errors": [
+        "(1,60): unexpected-end-tag",
+        "(1,61): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true,
+            "tbody": true,
+            "tr": true,
+            "td": true,
+            "svg svg": true,
+            "svg foreignObject": true,
+            "p": true,
+            "i": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "tbody",
+                        "children": [
+                          {
+                            "tag": "tr",
+                            "children": [
+                              {
+                                "tag": "td",
+                                "children": [
+                                  {
+                                    "tag": "svg",
+                                    "ns": "http://www.w3.org/2000/svg",
+                                    "children": [
+                                      {
+                                        "tag": "foreignObject",
+                                        "ns": "http://www.w3.org/2000/svg",
+                                        "children": [
+                                          {
+                                            "tag": "p",
+                                            "children": [
+                                              {
+                                                "tag": "i"
+                                              }
+                                            ]
+                                          },
+                                          {
+                                            "tag": "i",
+                                            "children": [
+                                              {
+                                                "text": "a"
+                                              }
+                                            ]
+                                          }
+                                        ]
+                                      }
+                                    ]
+                                  }
+                                ]
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><table><tbody><tr><td><svg><foreignObject><p><i></i></p><i>a</i></foreignObject></svg></td></tr></tbody></table></body></html>",
+        "noQuirksBodyHtml": "<table><tbody><tr><td><svg><foreignObject><p><i></i></p><i>a</i></foreignObject></svg></td></tr></tbody></table>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><math><mtext><p><i></p>a",
+      "errors": [
+        "(1,38): unexpected-end-tag",
+        "(1,39): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "math math": true,
+            "math mtext": true,
+            "p": true,
+            "i": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "math",
+                    "ns": "http://www.w3.org/1998/Math/MathML",
+                    "children": [
+                      {
+                        "tag": "mtext",
+                        "ns": "http://www.w3.org/1998/Math/MathML",
+                        "children": [
+                          {
+                            "tag": "p",
+                            "children": [
+                              {
+                                "tag": "i"
+                              }
+                            ]
+                          },
+                          {
+                            "tag": "i",
+                            "children": [
+                              {
+                                "text": "a"
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><math><mtext><p><i></i></p><i>a</i></mtext></math></body></html>",
+        "noQuirksBodyHtml": "<math><mtext><p><i></i></p><i>a</i></mtext></math>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><table><tr><td><math><mtext><p><i></p>a",
+      "errors": [
+        "(1,53): unexpected-end-tag",
+        "(1,54): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true,
+            "tbody": true,
+            "tr": true,
+            "td": true,
+            "math math": true,
+            "math mtext": true,
+            "p": true,
+            "i": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "tbody",
+                        "children": [
+                          {
+                            "tag": "tr",
+                            "children": [
+                              {
+                                "tag": "td",
+                                "children": [
+                                  {
+                                    "tag": "math",
+                                    "ns": "http://www.w3.org/1998/Math/MathML",
+                                    "children": [
+                                      {
+                                        "tag": "mtext",
+                                        "ns": "http://www.w3.org/1998/Math/MathML",
+                                        "children": [
+                                          {
+                                            "tag": "p",
+                                            "children": [
+                                              {
+                                                "tag": "i"
+                                              }
+                                            ]
+                                          },
+                                          {
+                                            "tag": "i",
+                                            "children": [
+                                              {
+                                                "text": "a"
+                                              }
+                                            ]
+                                          }
+                                        ]
+                                      }
+                                    ]
+                                  }
+                                ]
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><table><tbody><tr><td><math><mtext><p><i></i></p><i>a</i></mtext></math></td></tr></tbody></table></body></html>",
+        "noQuirksBodyHtml": "<table><tbody><tr><td><math><mtext><p><i></i></p><i>a</i></mtext></math></td></tr></tbody></table>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><body><div><!/div>a",
+      "errors": [
+        "(1,28): expected-dashes-or-doctype",
+        "(1,34): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "div": true
+          },
+          "doctype": true,
+          "comment": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "div",
+                    "children": [
+                      {
+                        "comment": "/div"
+                      },
+                      {
+                        "text": "a"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><div><!--/div-->a</div></body></html>",
+        "noQuirksBodyHtml": "<div><!--/div-->a</div>"
+      }
+    },
+    {
+      "data": "<button><p><button>",
+      "errors": [
+        "Line 1 Col 8 Unexpected start tag (button). Expected DOCTYPE.",
+        "Line 1 Col 19 Unexpected start tag (button) implies end tag (button).",
+        "Line 1 Col 19 Expected closing tag. Unexpected end of file."
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "button": true,
+            "p": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "button",
+                    "children": [
+                      {
+                        "tag": "p"
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "button"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><button><p></p></button><button></button></body></html>",
+        "noQuirksBodyHtml": "<button><p></p></button><button></button>"
+      }
+    }
+  ],
+  "tests3.dat": [
+    {
+      "data": "<head></head><style></style>",
+      "errors": [
+        "(1,6): expected-doctype-but-got-start-tag",
+        "(1,20): unexpected-start-tag-out-of-my-head"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "style": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "style"
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><style></style></head><body></body></html>",
+        "noQuirksBodyHtml": "<style></style>"
+      }
+    },
+    {
+      "data": "<head></head><script></script>",
+      "errors": [
+        "(1,6): expected-doctype-but-got-start-tag",
+        "(1,21): unexpected-start-tag-out-of-my-head"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script"
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><script></script></head><body></body></html>",
+        "noQuirksBodyHtml": "<script></script>"
+      }
+    },
+    {
+      "data": "<head></head><!-- --><style></style><!-- --><script></script>",
+      "errors": [
+        "(1,6): expected-doctype-but-got-start-tag",
+        "(1,28): unexpected-start-tag-out-of-my-head",
+        "(1,52): unexpected-start-tag-out-of-my-head"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "style": true,
+            "script": true,
+            "body": true
+          },
+          "comment": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "style"
+                  },
+                  {
+                    "tag": "script"
+                  }
+                ]
+              },
+              {
+                "comment": " "
+              },
+              {
+                "comment": " "
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><style></style><script></script></head><!-- --><!-- --><body></body></html>",
+        "noQuirksBodyHtml": "<!-- --><style></style><!-- --><script></script>"
+      }
+    },
+    {
+      "data": "<head></head><!-- -->x<style></style><!-- --><script></script>",
+      "errors": [
+        "(1,6): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "style": true,
+            "script": true
+          },
+          "comment": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "comment": " "
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "x"
+                  },
+                  {
+                    "tag": "style"
+                  },
+                  {
+                    "comment": " "
+                  },
+                  {
+                    "tag": "script"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><!-- --><body>x<style></style><!-- --><script></script></body></html>",
+        "noQuirksBodyHtml": "<!-- -->x<style></style><!-- --><script></script>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><html><head></head><body><pre>\n</pre></body></html>",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "pre": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "pre"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><pre></pre></body></html>",
+        "noQuirksBodyHtml": "<pre></pre>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><html><head></head><body><pre>\nfoo</pre></body></html>",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "pre": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "pre",
+                    "children": [
+                      {
+                        "text": "foo"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><pre>foo</pre></body></html>",
+        "noQuirksBodyHtml": "<pre>foo</pre>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><html><head></head><body><pre>\n\nfoo</pre></body></html>",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "pre": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "pre",
+                    "children": [
+                      {
+                        "text": "\nfoo"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><pre>\nfoo</pre></body></html>",
+        "noQuirksBodyHtml": "<pre>\nfoo</pre>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><html><head></head><body><pre>\nfoo\n</pre></body></html>",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "pre": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "pre",
+                    "children": [
+                      {
+                        "text": "foo\n"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><pre>foo\n</pre></body></html>",
+        "noQuirksBodyHtml": "<pre>foo\n</pre>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><html><head></head><body><pre>x</pre><span>\n</span></body></html>",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "pre": true,
+            "span": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "pre",
+                    "children": [
+                      {
+                        "text": "x"
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "span",
+                    "children": [
+                      {
+                        "text": "\n"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><pre>x</pre><span>\n</span></body></html>",
+        "noQuirksBodyHtml": "<pre>x</pre><span>\n</span>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><html><head></head><body><pre>x\ny</pre></body></html>",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "pre": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "pre",
+                    "children": [
+                      {
+                        "text": "x\ny"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><pre>x\ny</pre></body></html>",
+        "noQuirksBodyHtml": "<pre>x\ny</pre>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><html><head></head><body><pre>x<div>\ny</pre></body></html>",
+      "errors": [
+        "(2,7): end-tag-too-early"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "pre": true,
+            "div": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "pre",
+                    "children": [
+                      {
+                        "text": "x"
+                      },
+                      {
+                        "tag": "div",
+                        "children": [
+                          {
+                            "text": "\ny"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><pre>x<div>\ny</div></pre></body></html>",
+        "noQuirksBodyHtml": "<pre>x<div>\ny</div></pre>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><pre>&#x0a;&#x0a;A</pre>",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "pre": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "pre",
+                    "children": [
+                      {
+                        "text": "\nA"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><pre>\nA</pre></body></html>",
+        "noQuirksBodyHtml": "<pre>\nA</pre>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><HTML><META><HEAD></HEAD></HTML>",
+      "errors": [
+        "(1,33): two-heads-are-not-better-than-one"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "meta": true,
+            "body": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "meta"
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head><meta></head><body></body></html>",
+        "noQuirksBodyHtml": "<meta>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><HTML><HEAD><head></HEAD></HTML>",
+      "errors": [
+        "(1,33): two-heads-are-not-better-than-one"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body></body></html>",
+        "noQuirksBodyHtml": ""
+      }
+    },
+    {
+      "data": "<textarea>foo<span>bar</span><i>baz",
+      "errors": [
+        "(1,10): expected-doctype-but-got-start-tag",
+        "(1,35): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "textarea": true
+          },
+          "escaped": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "textarea",
+                    "children": [
+                      {
+                        "text": "foo<span>bar</span><i>baz",
+                        "escaped": true
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><textarea>foo&lt;span&gt;bar&lt;/span&gt;&lt;i&gt;baz</textarea></body></html>",
+        "noQuirksBodyHtml": "<textarea>foo&lt;span&gt;bar&lt;/span&gt;&lt;i&gt;baz</textarea>"
+      }
+    },
+    {
+      "data": "<title>foo<span>bar</em><i>baz",
+      "errors": [
+        "(1,7): expected-doctype-but-got-start-tag",
+        "(1,30): expected-named-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "title": true,
+            "body": true
+          },
+          "escaped": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "title",
+                    "children": [
+                      {
+                        "text": "foo<span>bar</em><i>baz",
+                        "escaped": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><title>foo&lt;span&gt;bar&lt;/em&gt;&lt;i&gt;baz</title></head><body></body></html>",
+        "noQuirksBodyHtml": "<title>foo&lt;span&gt;bar&lt;/em&gt;&lt;i&gt;baz</title>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><textarea>\n</textarea>",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "textarea": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "textarea"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><textarea></textarea></body></html>",
+        "noQuirksBodyHtml": "<textarea></textarea>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><textarea>\nfoo</textarea>",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "textarea": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "textarea",
+                    "children": [
+                      {
+                        "text": "foo"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><textarea>foo</textarea></body></html>",
+        "noQuirksBodyHtml": "<textarea>foo</textarea>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><textarea>\n\nfoo</textarea>",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "textarea": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "textarea",
+                    "children": [
+                      {
+                        "text": "\nfoo"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><textarea>\nfoo</textarea></body></html>",
+        "noQuirksBodyHtml": "<textarea>\nfoo</textarea>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><html><head></head><body><ul><li><div><p><li></ul></body></html>",
+      "errors": [
+        "(1,60): end-tag-too-early"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "ul": true,
+            "li": true,
+            "div": true,
+            "p": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "ul",
+                    "children": [
+                      {
+                        "tag": "li",
+                        "children": [
+                          {
+                            "tag": "div",
+                            "children": [
+                              {
+                                "tag": "p"
+                              }
+                            ]
+                          }
+                        ]
+                      },
+                      {
+                        "tag": "li"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><ul><li><div><p></p></div></li><li></li></ul></body></html>",
+        "noQuirksBodyHtml": "<ul><li><div><p></p></div></li><li></li></ul>"
+      }
+    },
+    {
+      "data": "<!doctype html><nobr><nobr><nobr>",
+      "errors": [
+        "(1,27): unexpected-start-tag-implies-end-tag",
+        "(1,33): unexpected-start-tag-implies-end-tag",
+        "(1,33): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "nobr": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "nobr"
+                  },
+                  {
+                    "tag": "nobr"
+                  },
+                  {
+                    "tag": "nobr"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><nobr></nobr><nobr></nobr><nobr></nobr></body></html>",
+        "noQuirksBodyHtml": "<nobr></nobr><nobr></nobr><nobr></nobr>"
+      }
+    },
+    {
+      "data": "<!doctype html><nobr><nobr></nobr><nobr>",
+      "errors": [
+        "(1,27): unexpected-start-tag-implies-end-tag",
+        "(1,40): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "nobr": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "nobr"
+                  },
+                  {
+                    "tag": "nobr"
+                  },
+                  {
+                    "tag": "nobr"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><nobr></nobr><nobr></nobr><nobr></nobr></body></html>",
+        "noQuirksBodyHtml": "<nobr></nobr><nobr></nobr><nobr></nobr>"
+      }
+    },
+    {
+      "data": "<!doctype html><html><body><p><table></table></body></html>",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "p": true,
+            "table": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "p"
+                  },
+                  {
+                    "tag": "table"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><p></p><table></table></body></html>",
+        "noQuirksBodyHtml": "<p></p><table></table>"
+      }
+    },
+    {
+      "data": "<p><table></table>",
+      "errors": [
+        "(1,3): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "p": true,
+            "table": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "p",
+                    "children": [
+                      {
+                        "tag": "table"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><p><table></table></p></body></html>",
+        "noQuirksBodyHtml": "<p></p><table></table>"
+      }
+    }
+  ],
+  "tests4.dat": [
+    {
+      "data": "direct div content",
+      "errors": [],
+      "fragment": {
+        "name": "div"
+      },
+      "document": {
+        "props": {
+          "tags": {}
+        },
+        "tree": [
+          {
+            "text": "direct div content"
+          }
+        ],
+        "html": "direct div content",
+        "noQuirksBodyHtml": "direct div content"
+      }
+    },
+    {
+      "data": "direct textarea content",
+      "errors": [],
+      "fragment": {
+        "name": "textarea"
+      },
+      "document": {
+        "props": {
+          "tags": {}
+        },
+        "tree": [
+          {
+            "text": "direct textarea content"
+          }
+        ],
+        "html": "direct textarea content",
+        "noQuirksBodyHtml": "direct textarea content"
+      }
+    },
+    {
+      "data": "textarea content with <em>pseudo</em> <foo>markup",
+      "errors": [],
+      "fragment": {
+        "name": "textarea"
+      },
+      "document": {
+        "props": {
+          "tags": {},
+          "escaped": true
+        },
+        "tree": [
+          {
+            "text": "textarea content with <em>pseudo</em> <foo>markup",
+            "escaped": true
+          }
+        ],
+        "html": "textarea content with &lt;em&gt;pseudo&lt;/em&gt; &lt;foo&gt;markup",
+        "noQuirksBodyHtml": "textarea content with <em>pseudo</em> <foo>markup</foo>"
+      }
+    },
+    {
+      "data": "this is &#x0043;DATA inside a <style> element",
+      "errors": [],
+      "fragment": {
+        "name": "style"
+      },
+      "document": {
+        "props": {
+          "tags": {},
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "text": "this is &#x0043;DATA inside a <style> element",
+            "no_escape": true
+          }
+        ],
+        "html": "this is &#x0043;DATA inside a <style> element",
+        "noQuirksBodyHtml": "this is CDATA inside a <style> element</style>"
+      }
+    },
+    {
+      "data": "</plaintext>",
+      "errors": [],
+      "fragment": {
+        "name": "plaintext"
+      },
+      "document": {
+        "props": {
+          "tags": {},
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "text": "</plaintext>",
+            "no_escape": true
+          }
+        ],
+        "html": "</plaintext>",
+        "noQuirksBodyHtml": ""
+      }
+    },
+    {
+      "data": "setting html's innerHTML",
+      "errors": [],
+      "fragment": {
+        "name": "html"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "head": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "head"
+          },
+          {
+            "tag": "body",
+            "children": [
+              {
+                "text": "setting html's innerHTML"
+              }
+            ]
+          }
+        ],
+        "html": "<head></head><body>setting html's innerHTML</body>",
+        "noQuirksBodyHtml": "setting html's innerHTML"
+      }
+    },
+    {
+      "data": "<title>setting head's innerHTML</title>",
+      "errors": [],
+      "fragment": {
+        "name": "head"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "title": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "title",
+            "children": [
+              {
+                "text": "setting head's innerHTML"
+              }
+            ]
+          }
+        ],
+        "html": "<title>setting head's innerHTML</title>",
+        "noQuirksBodyHtml": "<title>setting head's innerHTML</title>"
+      }
+    }
+  ],
+  "tests5.dat": [
+    {
+      "data": "<style> <!-- </style>x",
+      "errors": [
+        "(1,7): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "style": true,
+            "body": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "style",
+                    "children": [
+                      {
+                        "text": " <!-- ",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "x"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><style> <!-- </style></head><body>x</body></html>",
+        "noQuirksBodyHtml": "<style> <!-- </style>x"
+      }
+    },
+    {
+      "data": "<style> <!-- </style> --> </style>x",
+      "errors": [
+        "(1,7): expected-doctype-but-got-start-tag",
+        "(1,34): unexpected-end-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "style": true,
+            "body": true
+          },
+          "no_escape": true,
+          "escaped": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "style",
+                    "children": [
+                      {
+                        "text": " <!-- ",
+                        "no_escape": true
+                      }
+                    ]
+                  },
+                  {
+                    "text": " "
+                  }
+                ]
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "--> x",
+                    "escaped": true
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><style> <!-- </style> </head><body>--&gt; x</body></html>",
+        "noQuirksBodyHtml": "<style> <!-- </style> --&gt; x"
+      }
+    },
+    {
+      "data": "<style> <!--> </style>x",
+      "errors": [
+        "(1,7): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "style": true,
+            "body": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "style",
+                    "children": [
+                      {
+                        "text": " <!--> ",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "x"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><style> <!--> </style></head><body>x</body></html>",
+        "noQuirksBodyHtml": "<style> <!--> </style>x"
+      }
+    },
+    {
+      "data": "<style> <!---> </style>x",
+      "errors": [
+        "(1,7): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "style": true,
+            "body": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "style",
+                    "children": [
+                      {
+                        "text": " <!---> ",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "x"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><style> <!---> </style></head><body>x</body></html>",
+        "noQuirksBodyHtml": "<style> <!---> </style>x"
+      }
+    },
+    {
+      "data": "<iframe> <!---> </iframe>x",
+      "errors": [
+        "(1,8): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "iframe": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "iframe",
+                    "children": [
+                      {
+                        "text": " <!---> ",
+                        "no_escape": true
+                      }
+                    ]
+                  },
+                  {
+                    "text": "x"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><iframe> <!---> </iframe>x</body></html>",
+        "noQuirksBodyHtml": "<iframe> <!---> </iframe>x"
+      }
+    },
+    {
+      "data": "<iframe> <!--- </iframe>->x</iframe> --> </iframe>x",
+      "errors": [
+        "(1,8): expected-doctype-but-got-start-tag",
+        "(1,36): unexpected-end-tag",
+        "(1,50): unexpected-end-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "iframe": true
+          },
+          "no_escape": true,
+          "escaped": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "iframe",
+                    "children": [
+                      {
+                        "text": " <!--- ",
+                        "no_escape": true
+                      }
+                    ]
+                  },
+                  {
+                    "text": "->x --> x",
+                    "escaped": true
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><iframe> <!--- </iframe>-&gt;x --&gt; x</body></html>",
+        "noQuirksBodyHtml": "<iframe> <!--- </iframe>-&gt;x --&gt; x"
+      }
+    },
+    {
+      "data": "<script> <!-- </script> --> </script>x",
+      "errors": [
+        "(1,8): expected-doctype-but-got-start-tag",
+        "(1,37): unexpected-end-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "script": true,
+            "body": true
+          },
+          "no_escape": true,
+          "escaped": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": " <!-- ",
+                        "no_escape": true
+                      }
+                    ]
+                  },
+                  {
+                    "text": " "
+                  }
+                ]
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "--> x",
+                    "escaped": true
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><script> <!-- </script> </head><body>--&gt; x</body></html>",
+        "noQuirksBodyHtml": "<script> <!-- </script> --&gt; x"
+      }
+    },
+    {
+      "data": "<title> <!-- </title> --> </title>x",
+      "errors": [
+        "(1,7): expected-doctype-but-got-start-tag",
+        "(1,34): unexpected-end-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "title": true,
+            "body": true
+          },
+          "escaped": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "title",
+                    "children": [
+                      {
+                        "text": " <!-- ",
+                        "escaped": true
+                      }
+                    ]
+                  },
+                  {
+                    "text": " "
+                  }
+                ]
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "--> x",
+                    "escaped": true
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><title> &lt;!-- </title> </head><body>--&gt; x</body></html>",
+        "noQuirksBodyHtml": "<title> &lt;!-- </title> --&gt; x"
+      }
+    },
+    {
+      "data": "<textarea> <!--- </textarea>->x</textarea> --> </textarea>x",
+      "errors": [
+        "(1,10): expected-doctype-but-got-start-tag",
+        "(1,42): unexpected-end-tag",
+        "(1,58): unexpected-end-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "textarea": true
+          },
+          "escaped": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "textarea",
+                    "children": [
+                      {
+                        "text": " <!--- ",
+                        "escaped": true
+                      }
+                    ]
+                  },
+                  {
+                    "text": "->x --> x",
+                    "escaped": true
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><textarea> &lt;!--- </textarea>-&gt;x --&gt; x</body></html>",
+        "noQuirksBodyHtml": "<textarea> &lt;!--- </textarea>-&gt;x --&gt; x"
+      }
+    },
+    {
+      "data": "<style> <!</-- </style>x",
+      "errors": [
+        "(1,7): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "style": true,
+            "body": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "style",
+                    "children": [
+                      {
+                        "text": " <!</-- ",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "x"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><style> <!</-- </style></head><body>x</body></html>",
+        "noQuirksBodyHtml": "<style> <!</-- </style>x"
+      }
+    },
+    {
+      "data": "<p><xmp></xmp>",
+      "errors": [
+        "(1,3): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "p": true,
+            "xmp": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "p"
+                  },
+                  {
+                    "tag": "xmp"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><p></p><xmp></xmp></body></html>",
+        "noQuirksBodyHtml": "<p></p><xmp></xmp>"
+      }
+    },
+    {
+      "data": "<xmp> <!-- > --> </xmp>",
+      "errors": [
+        "(1,5): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "xmp": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "xmp",
+                    "children": [
+                      {
+                        "text": " <!-- > --> ",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><xmp> <!-- > --> </xmp></body></html>",
+        "noQuirksBodyHtml": "<xmp> <!-- > --> </xmp>"
+      }
+    },
+    {
+      "data": "<title>&amp;</title>",
+      "errors": [
+        "(1,7): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "title": true,
+            "body": true
+          },
+          "escaped": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "title",
+                    "children": [
+                      {
+                        "text": "&",
+                        "escaped": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><title>&amp;</title></head><body></body></html>",
+        "noQuirksBodyHtml": "<title>&amp;</title>"
+      }
+    },
+    {
+      "data": "<title><!--&amp;--></title>",
+      "errors": [
+        "(1,7): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "title": true,
+            "body": true
+          },
+          "escaped": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "title",
+                    "children": [
+                      {
+                        "text": "<!--&-->",
+                        "escaped": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><title>&lt;!--&amp;--&gt;</title></head><body></body></html>",
+        "noQuirksBodyHtml": "<title>&lt;!--&amp;--&gt;</title>"
+      }
+    },
+    {
+      "data": "<title><!--</title>",
+      "errors": [
+        "(1,7): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "title": true,
+            "body": true
+          },
+          "escaped": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "title",
+                    "children": [
+                      {
+                        "text": "<!--",
+                        "escaped": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><title>&lt;!--</title></head><body></body></html>",
+        "noQuirksBodyHtml": "<title>&lt;!--</title>"
+      }
+    },
+    {
+      "data": "<noscript><!--</noscript>--></noscript>",
+      "errors": [
+        "(1,10): expected-doctype-but-got-start-tag",
+        "(1,39): unexpected-end-tag"
+      ],
+      "script": "on",
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "noscript": true,
+            "body": true
+          },
+          "no_escape": true,
+          "escaped": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "noscript",
+                    "children": [
+                      {
+                        "text": "<!--",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "-->",
+                    "escaped": true
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><noscript><!--</noscript></head><body>--&gt;</body></html>",
+        "noQuirksBodyHtml": "<noscript><!--</noscript>--></noscript>"
+      }
+    },
+    {
+      "data": "<noscript><!--</noscript>--></noscript>",
+      "errors": [
+        "(1,10): expected-doctype-but-got-start-tag"
+      ],
+      "script": "off",
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "noscript": true,
+            "body": true
+          },
+          "comment": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "noscript",
+                    "children": [
+                      {
+                        "comment": "</noscript>"
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><noscript><!--</noscript>--></noscript></head><body></body></html>",
+        "noQuirksBodyHtml": "<noscript><!--</noscript>--></noscript>"
+      }
+    }
+  ],
+  "tests6.dat": [
+    {
+      "data": "<!doctype html></head> <head>",
+      "errors": [
+        "(1,29): unexpected-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "text": " "
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head> <body></body></html>",
+        "noQuirksBodyHtml": " "
+      }
+    },
+    {
+      "data": "<!doctype html><form><div></form><div>",
+      "errors": [
+        "(1,33): end-tag-too-early-ignored",
+        "(1,38): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "form": true,
+            "div": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "form",
+                    "children": [
+                      {
+                        "tag": "div",
+                        "children": [
+                          {
+                            "tag": "div"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><form><div><div></div></div></form></body></html>",
+        "noQuirksBodyHtml": "<form><div><div></div></div></form>"
+      }
+    },
+    {
+      "data": "<!doctype html><title>&amp;</title>",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "title": true,
+            "body": true
+          },
+          "doctype": true,
+          "escaped": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "title",
+                    "children": [
+                      {
+                        "text": "&",
+                        "escaped": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head><title>&amp;</title></head><body></body></html>",
+        "noQuirksBodyHtml": "<title>&amp;</title>"
+      }
+    },
+    {
+      "data": "<!doctype html><title><!--&amp;--></title>",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "title": true,
+            "body": true
+          },
+          "doctype": true,
+          "escaped": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "title",
+                    "children": [
+                      {
+                        "text": "<!--&-->",
+                        "escaped": true
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head><title>&lt;!--&amp;--&gt;</title></head><body></body></html>",
+        "noQuirksBodyHtml": "<title>&lt;!--&amp;--&gt;</title>"
+      }
+    },
+    {
+      "data": "<!doctype>",
+      "errors": [
+        "(1,9): need-space-after-doctype",
+        "(1,10): expected-doctype-name-but-got-right-bracket",
+        "(1,10): unknown-doctype"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": ""
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE ><html><head></head><body></body></html>",
+        "noQuirksBodyHtml": ""
+      }
+    },
+    {
+      "data": "<!---x",
+      "errors": [
+        "(1,6): eof-in-comment",
+        "(1,6): expected-doctype-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "comment": true
+        },
+        "tree": [
+          {
+            "comment": "-x"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!---x--><html><head></head><body></body></html>",
+        "noQuirksBodyHtml": "<!---x-->"
+      }
+    },
+    {
+      "data": "<body>\n<div>",
+      "errors": [
+        "(1,6): unexpected-start-tag",
+        "(2,5): expected-closing-tag-but-got-eof"
+      ],
+      "fragment": {
+        "name": "div"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "div": true
+          }
+        },
+        "tree": [
+          {
+            "text": "\n"
+          },
+          {
+            "tag": "div"
+          }
+        ],
+        "html": "\n<div></div>",
+        "noQuirksBodyHtml": "\n<div></div>"
+      }
+    },
+    {
+      "data": "<frameset></frameset>\nfoo",
+      "errors": [
+        "(1,10): expected-doctype-but-got-start-tag",
+        "(2,1): unexpected-char-after-frameset",
+        "(2,2): unexpected-char-after-frameset",
+        "(2,3): unexpected-char-after-frameset"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "frameset": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "frameset"
+              },
+              {
+                "text": "\n"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><frameset></frameset>\n</html>",
+        "noQuirksBodyHtml": "\nfoo"
+      }
+    },
+    {
+      "data": "<frameset></frameset>\n<noframes>",
+      "errors": [
+        "(1,10): expected-doctype-but-got-start-tag",
+        "(2,10): expected-named-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "frameset": true,
+            "noframes": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "frameset"
+              },
+              {
+                "text": "\n"
+              },
+              {
+                "tag": "noframes"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><frameset></frameset>\n<noframes></noframes></html>",
+        "noQuirksBodyHtml": "\n<noframes></noframes>"
+      }
+    },
+    {
+      "data": "<frameset></frameset>\n<div>",
+      "errors": [
+        "(1,10): expected-doctype-but-got-start-tag",
+        "(2,5): unexpected-start-tag-after-frameset"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "frameset": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "frameset"
+              },
+              {
+                "text": "\n"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><frameset></frameset>\n</html>",
+        "noQuirksBodyHtml": "\n<div></div>"
+      }
+    },
+    {
+      "data": "<frameset></frameset>\n</html>",
+      "errors": [
+        "(1,10): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "frameset": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "frameset"
+              },
+              {
+                "text": "\n"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><frameset></frameset>\n</html>",
+        "noQuirksBodyHtml": "\n"
+      }
+    },
+    {
+      "data": "<frameset></frameset>\n</div>",
+      "errors": [
+        "(1,10): expected-doctype-but-got-start-tag",
+        "(2,6): unexpected-end-tag-after-frameset"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "frameset": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "frameset"
+              },
+              {
+                "text": "\n"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><frameset></frameset>\n</html>",
+        "noQuirksBodyHtml": "\n"
+      }
+    },
+    {
+      "data": "<form><form>",
+      "errors": [
+        "(1,6): expected-doctype-but-got-start-tag",
+        "(1,12): unexpected-start-tag",
+        "(1,12): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "form": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "form"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><form></form></body></html>",
+        "noQuirksBodyHtml": "<form></form>"
+      }
+    },
+    {
+      "data": "<button><button>",
+      "errors": [
+        "(1,8): expected-doctype-but-got-start-tag",
+        "(1,16): unexpected-start-tag-implies-end-tag",
+        "(1,16): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "button": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "button"
+                  },
+                  {
+                    "tag": "button"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><button></button><button></button></body></html>",
+        "noQuirksBodyHtml": "<button></button><button></button>"
+      }
+    },
+    {
+      "data": "<table><tr><td></th>",
+      "errors": [
+        "(1,7): expected-doctype-but-got-start-tag",
+        "(1,20): unexpected-end-tag",
+        "(1,20): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true,
+            "tbody": true,
+            "tr": true,
+            "td": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "tbody",
+                        "children": [
+                          {
+                            "tag": "tr",
+                            "children": [
+                              {
+                                "tag": "td"
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><table><tbody><tr><td></td></tr></tbody></table></body></html>",
+        "noQuirksBodyHtml": "<table><tbody><tr><td></td></tr></tbody></table>"
+      }
+    },
+    {
+      "data": "<table><caption><td>",
+      "errors": [
+        "(1,7): expected-doctype-but-got-start-tag",
+        "(1,20): unexpected-cell-in-table-body",
+        "(1,20): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true,
+            "caption": true,
+            "tbody": true,
+            "tr": true,
+            "td": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "caption"
+                      },
+                      {
+                        "tag": "tbody",
+                        "children": [
+                          {
+                            "tag": "tr",
+                            "children": [
+                              {
+                                "tag": "td"
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><table><caption></caption><tbody><tr><td></td></tr></tbody></table></body></html>",
+        "noQuirksBodyHtml": "<table><caption></caption><tbody><tr><td></td></tr></tbody></table>"
+      }
+    },
+    {
+      "data": "<table><caption><div>",
+      "errors": [
+        "(1,7): expected-doctype-but-got-start-tag",
+        "(1,21): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true,
+            "caption": true,
+            "div": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "caption",
+                        "children": [
+                          {
+                            "tag": "div"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><table><caption><div></div></caption></table></body></html>",
+        "noQuirksBodyHtml": "<table><caption><div></div></caption></table>"
+      }
+    },
+    {
+      "data": "</caption><div>",
+      "errors": [
+        "(1,10): XXX-undefined-error",
+        "(1,15): expected-closing-tag-but-got-eof"
+      ],
+      "fragment": {
+        "name": "caption"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "div": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "div"
+          }
+        ],
+        "html": "<div></div>",
+        "noQuirksBodyHtml": "<div></div>"
+      }
+    },
+    {
+      "data": "<table><caption><div></caption>",
+      "errors": [
+        "(1,7): expected-doctype-but-got-start-tag",
+        "(1,31): expected-one-end-tag-but-got-another",
+        "(1,31): eof-in-table"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true,
+            "caption": true,
+            "div": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "caption",
+                        "children": [
+                          {
+                            "tag": "div"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><table><caption><div></div></caption></table></body></html>",
+        "noQuirksBodyHtml": "<table><caption><div></div></caption></table>"
+      }
+    },
+    {
+      "data": "<table><caption></table>",
+      "errors": [
+        "(1,7): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true,
+            "caption": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "caption"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><table><caption></caption></table></body></html>",
+        "noQuirksBodyHtml": "<table><caption></caption></table>"
+      }
+    },
+    {
+      "data": "</table><div>",
+      "errors": [
+        "(1,8): unexpected-end-tag",
+        "(1,13): expected-closing-tag-but-got-eof"
+      ],
+      "fragment": {
+        "name": "caption"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "div": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "div"
+          }
+        ],
+        "html": "<div></div>",
+        "noQuirksBodyHtml": "<div></div>"
+      }
+    },
+    {
+      "data": "<table><caption></body></col></colgroup></html></tbody></td></tfoot></th></thead></tr>",
+      "errors": [
+        "(1,7): expected-doctype-but-got-start-tag",
+        "(1,23): unexpected-end-tag",
+        "(1,29): unexpected-end-tag",
+        "(1,40): unexpected-end-tag",
+        "(1,47): unexpected-end-tag",
+        "(1,55): unexpected-end-tag",
+        "(1,60): unexpected-end-tag",
+        "(1,68): unexpected-end-tag",
+        "(1,73): unexpected-end-tag",
+        "(1,81): unexpected-end-tag",
+        "(1,86): unexpected-end-tag",
+        "(1,86): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true,
+            "caption": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "caption"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><table><caption></caption></table></body></html>",
+        "noQuirksBodyHtml": "<table><caption></caption></table>"
+      }
+    },
+    {
+      "data": "<table><caption><div></div>",
+      "errors": [
+        "(1,7): expected-doctype-but-got-start-tag",
+        "(1,27): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true,
+            "caption": true,
+            "div": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "caption",
+                        "children": [
+                          {
+                            "tag": "div"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><table><caption><div></div></caption></table></body></html>",
+        "noQuirksBodyHtml": "<table><caption><div></div></caption></table>"
+      }
+    },
+    {
+      "data": "<table><tr><td></body></caption></col></colgroup></html>",
+      "errors": [
+        "(1,7): expected-doctype-but-got-start-tag",
+        "(1,22): unexpected-end-tag",
+        "(1,32): unexpected-end-tag",
+        "(1,38): unexpected-end-tag",
+        "(1,49): unexpected-end-tag",
+        "(1,56): unexpected-end-tag",
+        "(1,56): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true,
+            "tbody": true,
+            "tr": true,
+            "td": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "tbody",
+                        "children": [
+                          {
+                            "tag": "tr",
+                            "children": [
+                              {
+                                "tag": "td"
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><table><tbody><tr><td></td></tr></tbody></table></body></html>",
+        "noQuirksBodyHtml": "<table><tbody><tr><td></td></tr></tbody></table>"
+      }
+    },
+    {
+      "data": "</table></tbody></tfoot></thead></tr><div>",
+      "errors": [
+        "(1,8): unexpected-end-tag",
+        "(1,16): unexpected-end-tag",
+        "(1,24): unexpected-end-tag",
+        "(1,32): unexpected-end-tag",
+        "(1,37): unexpected-end-tag",
+        "(1,42): expected-closing-tag-but-got-eof"
+      ],
+      "fragment": {
+        "name": "td"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "div": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "div"
+          }
+        ],
+        "html": "<div></div>",
+        "noQuirksBodyHtml": "<div></div>"
+      }
+    },
+    {
+      "data": "<table><colgroup>foo",
+      "errors": [
+        "(1,7): expected-doctype-but-got-start-tag",
+        "(1,18): foster-parenting-character-in-table",
+        "(1,19): foster-parenting-character-in-table",
+        "(1,20): foster-parenting-character-in-table",
+        "(1,20): eof-in-table"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true,
+            "colgroup": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "foo"
+                  },
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "colgroup"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>foo<table><colgroup></colgroup></table></body></html>",
+        "noQuirksBodyHtml": "foo<table><colgroup></colgroup></table>"
+      }
+    },
+    {
+      "data": "foo<col>",
+      "errors": [
+        "(1,1): unexpected-character-in-colgroup",
+        "(1,2): unexpected-character-in-colgroup",
+        "(1,3): unexpected-character-in-colgroup"
+      ],
+      "fragment": {
+        "name": "colgroup"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "col": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "col"
+          }
+        ],
+        "html": "<col>",
+        "noQuirksBodyHtml": "foo"
+      }
+    },
+    {
+      "data": "<table><colgroup></col>",
+      "errors": [
+        "(1,7): expected-doctype-but-got-start-tag",
+        "(1,23): no-end-tag",
+        "(1,23): eof-in-table"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true,
+            "colgroup": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "colgroup"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><table><colgroup></colgroup></table></body></html>",
+        "noQuirksBodyHtml": "<table><colgroup></colgroup></table>"
+      }
+    },
+    {
+      "data": "<frameset><div>",
+      "errors": [
+        "(1,10): expected-doctype-but-got-start-tag",
+        "(1,15): unexpected-start-tag-in-frameset",
+        "(1,15): eof-in-frameset"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "frameset": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "frameset"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><frameset></frameset></html>",
+        "noQuirksBodyHtml": "<div></div>"
+      }
+    },
+    {
+      "data": "</frameset><frame>",
+      "errors": [
+        "(1,11): unexpected-frameset-in-frameset-innerhtml"
+      ],
+      "fragment": {
+        "name": "frameset"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "frame": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "frame"
+          }
+        ],
+        "html": "<frame>",
+        "noQuirksBodyHtml": ""
+      }
+    },
+    {
+      "data": "<frameset></div>",
+      "errors": [
+        "(1,10): expected-doctype-but-got-start-tag",
+        "(1,16): unexpected-end-tag-in-frameset",
+        "(1,16): eof-in-frameset"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "frameset": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "frameset"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><frameset></frameset></html>",
+        "noQuirksBodyHtml": ""
+      }
+    },
+    {
+      "data": "</body><div>",
+      "errors": [
+        "(1,7): unexpected-close-tag",
+        "(1,12): expected-closing-tag-but-got-eof"
+      ],
+      "fragment": {
+        "name": "body"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "div": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "div"
+          }
+        ],
+        "html": "<div></div>",
+        "noQuirksBodyHtml": "<div></div>"
+      }
+    },
+    {
+      "data": "<table><tr><div>",
+      "errors": [
+        "(1,7): expected-doctype-but-got-start-tag",
+        "(1,16): unexpected-start-tag-implies-table-voodoo",
+        "(1,16): eof-in-table"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "div": true,
+            "table": true,
+            "tbody": true,
+            "tr": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "div"
+                  },
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "tbody",
+                        "children": [
+                          {
+                            "tag": "tr"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><div></div><table><tbody><tr></tr></tbody></table></body></html>",
+        "noQuirksBodyHtml": "<div></div><table><tbody><tr></tr></tbody></table>"
+      }
+    },
+    {
+      "data": "</tr><td>",
+      "errors": [
+        "(1,5): unexpected-end-tag"
+      ],
+      "fragment": {
+        "name": "tr"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "td": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "td"
+          }
+        ],
+        "html": "<td></td>",
+        "noQuirksBodyHtml": ""
+      }
+    },
+    {
+      "data": "</tbody></tfoot></thead><td>",
+      "errors": [
+        "(1,8): unexpected-end-tag",
+        "(1,16): unexpected-end-tag",
+        "(1,24): unexpected-end-tag"
+      ],
+      "fragment": {
+        "name": "tr"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "td": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "td"
+          }
+        ],
+        "html": "<td></td>",
+        "noQuirksBodyHtml": ""
+      }
+    },
+    {
+      "data": "<table><tr><div><td>",
+      "errors": [
+        "(1,7): expected-doctype-but-got-start-tag",
+        "(1,16): foster-parenting-start-tag",
+        "(1,20): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "div": true,
+            "table": true,
+            "tbody": true,
+            "tr": true,
+            "td": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "div"
+                  },
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "tbody",
+                        "children": [
+                          {
+                            "tag": "tr",
+                            "children": [
+                              {
+                                "tag": "td"
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><div></div><table><tbody><tr><td></td></tr></tbody></table></body></html>",
+        "noQuirksBodyHtml": "<div></div><table><tbody><tr><td></td></tr></tbody></table>"
+      }
+    },
+    {
+      "data": "<caption><col><colgroup><tbody><tfoot><thead><tr>",
+      "errors": [
+        "(1,9): unexpected-start-tag",
+        "(1,14): unexpected-start-tag",
+        "(1,24): unexpected-start-tag",
+        "(1,31): unexpected-start-tag",
+        "(1,38): unexpected-start-tag",
+        "(1,45): unexpected-start-tag"
+      ],
+      "fragment": {
+        "name": "tbody"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "tr": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "tr"
+          }
+        ],
+        "html": "<tr></tr>",
+        "noQuirksBodyHtml": ""
+      }
+    },
+    {
+      "data": "<table><tbody></thead>",
+      "errors": [
+        "(1,7): expected-doctype-but-got-start-tag",
+        "(1,22): unexpected-end-tag-in-table-body",
+        "(1,22): eof-in-table"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true,
+            "tbody": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "tbody"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><table><tbody></tbody></table></body></html>",
+        "noQuirksBodyHtml": "<table><tbody></tbody></table>"
+      }
+    },
+    {
+      "data": "</table><tr>",
+      "errors": [
+        "(1,8): unexpected-end-tag"
+      ],
+      "fragment": {
+        "name": "tbody"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "tr": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "tr"
+          }
+        ],
+        "html": "<tr></tr>",
+        "noQuirksBodyHtml": ""
+      }
+    },
+    {
+      "data": "<table><tbody></body></caption></col></colgroup></html></td></th></tr>",
+      "errors": [
+        "(1,7): expected-doctype-but-got-start-tag",
+        "(1,21): unexpected-end-tag-in-table-body",
+        "(1,31): unexpected-end-tag-in-table-body",
+        "(1,37): unexpected-end-tag-in-table-body",
+        "(1,48): unexpected-end-tag-in-table-body",
+        "(1,55): unexpected-end-tag-in-table-body",
+        "(1,60): unexpected-end-tag-in-table-body",
+        "(1,65): unexpected-end-tag-in-table-body",
+        "(1,70): unexpected-end-tag-in-table-body",
+        "(1,70): eof-in-table"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true,
+            "tbody": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "tbody"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><table><tbody></tbody></table></body></html>",
+        "noQuirksBodyHtml": "<table><tbody></tbody></table>"
+      }
+    },
+    {
+      "data": "<table><tbody></div>",
+      "errors": [
+        "(1,7): expected-doctype-but-got-start-tag",
+        "(1,20): unexpected-end-tag-implies-table-voodoo",
+        "(1,20): end-tag-too-early",
+        "(1,20): eof-in-table"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true,
+            "tbody": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "tbody"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><table><tbody></tbody></table></body></html>",
+        "noQuirksBodyHtml": "<table><tbody></tbody></table>"
+      }
+    },
+    {
+      "data": "<table><table>",
+      "errors": [
+        "(1,7): expected-doctype-but-got-start-tag",
+        "(1,14): unexpected-start-tag-implies-end-tag",
+        "(1,14): eof-in-table"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "table"
+                  },
+                  {
+                    "tag": "table"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><table></table><table></table></body></html>",
+        "noQuirksBodyHtml": "<table></table><table></table>"
+      }
+    },
+    {
+      "data": "<table></body></caption></col></colgroup></html></tbody></td></tfoot></th></thead></tr>",
+      "errors": [
+        "(1,7): expected-doctype-but-got-start-tag",
+        "(1,14): unexpected-end-tag",
+        "(1,24): unexpected-end-tag",
+        "(1,30): unexpected-end-tag",
+        "(1,41): unexpected-end-tag",
+        "(1,48): unexpected-end-tag",
+        "(1,56): unexpected-end-tag",
+        "(1,61): unexpected-end-tag",
+        "(1,69): unexpected-end-tag",
+        "(1,74): unexpected-end-tag",
+        "(1,82): unexpected-end-tag",
+        "(1,87): unexpected-end-tag",
+        "(1,87): eof-in-table"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "table"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><table></table></body></html>",
+        "noQuirksBodyHtml": "<table></table>"
+      }
+    },
+    {
+      "data": "</table><tr>",
+      "errors": [
+        "(1,8): unexpected-end-tag"
+      ],
+      "fragment": {
+        "name": "table"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "tbody": true,
+            "tr": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "tbody",
+            "children": [
+              {
+                "tag": "tr"
+              }
+            ]
+          }
+        ],
+        "html": "<tbody><tr></tr></tbody>",
+        "noQuirksBodyHtml": ""
+      }
+    },
+    {
+      "data": "<body></body></html>",
+      "errors": [
+        "(1,20): unexpected-end-tag-after-body-innerhtml"
+      ],
+      "fragment": {
+        "name": "html"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "head": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "head"
+          },
+          {
+            "tag": "body"
+          }
+        ],
+        "html": "<head></head><body></body>",
+        "noQuirksBodyHtml": ""
+      }
+    },
+    {
+      "data": "<html><frameset></frameset></html> ",
+      "errors": [
+        "(1,6): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "frameset": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "frameset"
+              },
+              {
+                "text": " "
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><frameset></frameset> </html>",
+        "noQuirksBodyHtml": " "
+      }
+    },
+    {
+      "data": "<!DOCTYPE html PUBLIC \"-//W3C//DTD HTML 4.01//EN\"><html></html>",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html \"-//W3C//DTD HTML 4.01//EN\" \"\""
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body></body></html>",
+        "noQuirksBodyHtml": ""
+      }
+    },
+    {
+      "data": "<param><frameset></frameset>",
+      "errors": [
+        "(1,7): expected-doctype-but-got-start-tag",
+        "(1,17): unexpected-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "frameset": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "frameset"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><frameset></frameset></html>",
+        "noQuirksBodyHtml": "<param>"
+      }
+    },
+    {
+      "data": "<source><frameset></frameset>",
+      "errors": [
+        "(1,8): expected-doctype-but-got-start-tag",
+        "(1,18): unexpected-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "frameset": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "frameset"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><frameset></frameset></html>",
+        "noQuirksBodyHtml": "<source>"
+      }
+    },
+    {
+      "data": "<track><frameset></frameset>",
+      "errors": [
+        "(1,7): expected-doctype-but-got-start-tag",
+        "(1,17): unexpected-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "frameset": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "frameset"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><frameset></frameset></html>",
+        "noQuirksBodyHtml": "<track>"
+      }
+    },
+    {
+      "data": "</html><frameset></frameset>",
+      "errors": [
+        "(1,7): expected-doctype-but-got-end-tag",
+        "(1,17): expected-eof-but-got-start-tag",
+        "(1,17): unexpected-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "frameset": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "frameset"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><frameset></frameset></html>",
+        "noQuirksBodyHtml": ""
+      }
+    },
+    {
+      "data": "</body><frameset></frameset>",
+      "errors": [
+        "(1,7): expected-doctype-but-got-end-tag",
+        "(1,17): unexpected-start-tag-after-body",
+        "(1,17): unexpected-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "frameset": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "frameset"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><frameset></frameset></html>",
+        "noQuirksBodyHtml": ""
+      }
+    }
+  ],
+  "tests7.dat": [
+    {
+      "data": "<!doctype html><body><title>X</title>",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "title": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "title",
+                    "children": [
+                      {
+                        "text": "X"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><title>X</title></body></html>",
+        "noQuirksBodyHtml": "<title>X</title>"
+      }
+    },
+    {
+      "data": "<!doctype html><table><title>X</title></table>",
+      "errors": [
+        "(1,29): unexpected-start-tag-implies-table-voodoo"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "title": true,
+            "table": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "title",
+                    "children": [
+                      {
+                        "text": "X"
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "table"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><title>X</title><table></table></body></html>",
+        "noQuirksBodyHtml": "<title>X</title><table></table>"
+      }
+    },
+    {
+      "data": "<!doctype html><head></head><title>X</title>",
+      "errors": [
+        "(1,35): unexpected-start-tag-out-of-my-head"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "title": true,
+            "body": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "title",
+                    "children": [
+                      {
+                        "text": "X"
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head><title>X</title></head><body></body></html>",
+        "noQuirksBodyHtml": "<title>X</title>"
+      }
+    },
+    {
+      "data": "<!doctype html></head><title>X</title>",
+      "errors": [
+        "(1,29): unexpected-start-tag-out-of-my-head"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "title": true,
+            "body": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "title",
+                    "children": [
+                      {
+                        "text": "X"
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head><title>X</title></head><body></body></html>",
+        "noQuirksBodyHtml": "<title>X</title>"
+      }
+    },
+    {
+      "data": "<!doctype html><table><meta></table>",
+      "errors": [
+        "(1,28): unexpected-start-tag-implies-table-voodoo"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "meta": true,
+            "table": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "meta"
+                  },
+                  {
+                    "tag": "table"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><meta><table></table></body></html>",
+        "noQuirksBodyHtml": "<meta><table></table>"
+      }
+    },
+    {
+      "data": "<!doctype html><table>X<tr><td><table> <meta></table></table>",
+      "errors": [
+        "unexpected text in table",
+        "(1,45): unexpected-start-tag-implies-table-voodoo"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true,
+            "tbody": true,
+            "tr": true,
+            "td": true,
+            "meta": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "X"
+                  },
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "tbody",
+                        "children": [
+                          {
+                            "tag": "tr",
+                            "children": [
+                              {
+                                "tag": "td",
+                                "children": [
+                                  {
+                                    "tag": "meta"
+                                  },
+                                  {
+                                    "tag": "table",
+                                    "children": [
+                                      {
+                                        "text": " "
+                                      }
+                                    ]
+                                  }
+                                ]
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body>X<table><tbody><tr><td><meta><table> </table></td></tr></tbody></table></body></html>",
+        "noQuirksBodyHtml": "X<table><tbody><tr><td><meta><table> </table></td></tr></tbody></table>"
+      }
+    },
+    {
+      "data": "<!doctype html><html> <head>",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body></body></html>",
+        "noQuirksBodyHtml": " "
+      }
+    },
+    {
+      "data": "<!doctype html> <head>",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body></body></html>",
+        "noQuirksBodyHtml": " "
+      }
+    },
+    {
+      "data": "<!doctype html><table><style> <tr>x </style> </table>",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true,
+            "style": true
+          },
+          "doctype": true,
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "style",
+                        "children": [
+                          {
+                            "text": " <tr>x ",
+                            "no_escape": true
+                          }
+                        ]
+                      },
+                      {
+                        "text": " "
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><table><style> <tr>x </style> </table></body></html>",
+        "noQuirksBodyHtml": "<table><style> <tr>x </style> </table>"
+      }
+    },
+    {
+      "data": "<!doctype html><table><TBODY><script> <tr>x </script> </table>",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true,
+            "tbody": true,
+            "script": true
+          },
+          "doctype": true,
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "tbody",
+                        "children": [
+                          {
+                            "tag": "script",
+                            "children": [
+                              {
+                                "text": " <tr>x ",
+                                "no_escape": true
+                              }
+                            ]
+                          },
+                          {
+                            "text": " "
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><table><tbody><script> <tr>x </script> </tbody></table></body></html>",
+        "noQuirksBodyHtml": "<table><tbody><script> <tr>x </script> </tbody></table>"
+      }
+    },
+    {
+      "data": "<!doctype html><p><applet><p>X</p></applet>",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "p": true,
+            "applet": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "p",
+                    "children": [
+                      {
+                        "tag": "applet",
+                        "children": [
+                          {
+                            "tag": "p",
+                            "children": [
+                              {
+                                "text": "X"
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><p><applet><p>X</p></applet></p></body></html>",
+        "noQuirksBodyHtml": "<p><applet><p>X</p></applet></p>"
+      }
+    },
+    {
+      "data": "<!doctype html><p><object type=\"application/x-non-existant-plugin\"><p>X</p></object>",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "p": true,
+            "object": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "p",
+                    "children": [
+                      {
+                        "tag": "object",
+                        "attrs": [
+                          {
+                            "name": "type",
+                            "value": "application/x-non-existant-plugin"
+                          }
+                        ],
+                        "children": [
+                          {
+                            "tag": "p",
+                            "children": [
+                              {
+                                "text": "X"
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><p><object type=\"application/x-non-existant-plugin\"><p>X</p></object></p></body></html>",
+        "noQuirksBodyHtml": "<p><object type=\"application/x-non-existant-plugin\"><p>X</p></object></p>"
+      }
+    },
+    {
+      "data": "<!doctype html><listing>\nX</listing>",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "listing": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "listing",
+                    "children": [
+                      {
+                        "text": "X"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><listing>X</listing></body></html>",
+        "noQuirksBodyHtml": "<listing>X</listing>"
+      }
+    },
+    {
+      "data": "<!doctype html><select><input>X",
+      "errors": [
+        "(1,30): unexpected-input-in-select"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "select": true,
+            "input": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "select"
+                  },
+                  {
+                    "tag": "input"
+                  },
+                  {
+                    "text": "X"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><select></select><input>X</body></html>",
+        "noQuirksBodyHtml": "<select></select><input>X"
+      }
+    },
+    {
+      "data": "<!doctype html><select><select>X",
+      "errors": [
+        "(1,31): unexpected-select-in-select"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "select": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "select"
+                  },
+                  {
+                    "text": "X"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><select></select>X</body></html>",
+        "noQuirksBodyHtml": "<select></select>X"
+      }
+    },
+    {
+      "data": "<!doctype html><table><input type=hidDEN></table>",
+      "errors": [
+        "(1,41): unexpected-hidden-input-in-table"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true,
+            "input": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "input",
+                        "attrs": [
+                          {
+                            "name": "type",
+                            "value": "hidDEN"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><table><input type=\"hidDEN\"></table></body></html>",
+        "noQuirksBodyHtml": "<table><input type=\"hidDEN\"></table>"
+      }
+    },
+    {
+      "data": "<!doctype html><table>X<input type=hidDEN></table>",
+      "errors": [
+        "(1,23): foster-parenting-character",
+        "(1,42): unexpected-hidden-input-in-table"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true,
+            "input": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "X"
+                  },
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "input",
+                        "attrs": [
+                          {
+                            "name": "type",
+                            "value": "hidDEN"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body>X<table><input type=\"hidDEN\"></table></body></html>",
+        "noQuirksBodyHtml": "X<table><input type=\"hidDEN\"></table>"
+      }
+    },
+    {
+      "data": "<!doctype html><table>  <input type=hidDEN></table>",
+      "errors": [
+        "(1,43): unexpected-hidden-input-in-table"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true,
+            "input": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "text": "  "
+                      },
+                      {
+                        "tag": "input",
+                        "attrs": [
+                          {
+                            "name": "type",
+                            "value": "hidDEN"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><table>  <input type=\"hidDEN\"></table></body></html>",
+        "noQuirksBodyHtml": "<table>  <input type=\"hidDEN\"></table>"
+      }
+    },
+    {
+      "data": "<!doctype html><table>  <input type='hidDEN'></table>",
+      "errors": [
+        "(1,45): unexpected-hidden-input-in-table"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true,
+            "input": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "text": "  "
+                      },
+                      {
+                        "tag": "input",
+                        "attrs": [
+                          {
+                            "name": "type",
+                            "value": "hidDEN"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><table>  <input type=\"hidDEN\"></table></body></html>",
+        "noQuirksBodyHtml": "<table>  <input type=\"hidDEN\"></table>"
+      }
+    },
+    {
+      "data": "<!doctype html><table><input type=\" hidden\"><input type=hidDEN></table>",
+      "errors": [
+        "(1,44): unexpected-start-tag-implies-table-voodoo",
+        "(1,63): unexpected-hidden-input-in-table"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "input": true,
+            "table": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "input",
+                    "attrs": [
+                      {
+                        "name": "type",
+                        "value": " hidden"
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "input",
+                        "attrs": [
+                          {
+                            "name": "type",
+                            "value": "hidDEN"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><input type=\" hidden\"><table><input type=\"hidDEN\"></table></body></html>",
+        "noQuirksBodyHtml": "<input type=\" hidden\"><table><input type=\"hidDEN\"></table>"
+      }
+    },
+    {
+      "data": "<!doctype html><table><select>X<tr>",
+      "errors": [
+        "(1,30): unexpected-start-tag-implies-table-voodoo",
+        "(1,35): unexpected-table-element-start-tag-in-select-in-table",
+        "(1,35): eof-in-table"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "select": true,
+            "table": true,
+            "tbody": true,
+            "tr": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "select",
+                    "children": [
+                      {
+                        "text": "X"
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "tbody",
+                        "children": [
+                          {
+                            "tag": "tr"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><select>X</select><table><tbody><tr></tr></tbody></table></body></html>",
+        "noQuirksBodyHtml": "<select>X</select><table><tbody><tr></tr></tbody></table>"
+      }
+    },
+    {
+      "data": "<!doctype html><select>X</select>",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "select": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "select",
+                    "children": [
+                      {
+                        "text": "X"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><select>X</select></body></html>",
+        "noQuirksBodyHtml": "<select>X</select>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE hTmL><html></html>",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body></body></html>",
+        "noQuirksBodyHtml": ""
+      }
+    },
+    {
+      "data": "<!DOCTYPE HTML><html></html>",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body></body></html>",
+        "noQuirksBodyHtml": ""
+      }
+    },
+    {
+      "data": "<body>X</body></body>",
+      "errors": [
+        "(1,21): unexpected-end-tag-after-body"
+      ],
+      "fragment": {
+        "name": "html"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "head": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "head"
+          },
+          {
+            "tag": "body",
+            "children": [
+              {
+                "text": "X"
+              }
+            ]
+          }
+        ],
+        "html": "<head></head><body>X</body>",
+        "noQuirksBodyHtml": "X"
+      }
+    },
+    {
+      "data": "<div><p>a</x> b",
+      "errors": [
+        "(1,5): expected-doctype-but-got-start-tag",
+        "(1,13): unexpected-end-tag",
+        "(1,15): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "div": true,
+            "p": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "div",
+                    "children": [
+                      {
+                        "tag": "p",
+                        "children": [
+                          {
+                            "text": "a b"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><div><p>a b</p></div></body></html>",
+        "noQuirksBodyHtml": "<div><p>a b</p></div>"
+      }
+    },
+    {
+      "data": "<table><tr><td><code></code> </table>",
+      "errors": [
+        "(1,7): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true,
+            "tbody": true,
+            "tr": true,
+            "td": true,
+            "code": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "tbody",
+                        "children": [
+                          {
+                            "tag": "tr",
+                            "children": [
+                              {
+                                "tag": "td",
+                                "children": [
+                                  {
+                                    "tag": "code"
+                                  },
+                                  {
+                                    "text": " "
+                                  }
+                                ]
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><table><tbody><tr><td><code></code> </td></tr></tbody></table></body></html>",
+        "noQuirksBodyHtml": "<table><tbody><tr><td><code></code> </td></tr></tbody></table>"
+      }
+    },
+    {
+      "data": "<table><b><tr><td>aaa</td></tr>bbb</table>ccc",
+      "errors": [
+        "(1,7): expected-doctype-but-got-start-tag",
+        "(1,10): foster-parenting-start-tag",
+        "(1,32): foster-parenting-character",
+        "(1,33): foster-parenting-character",
+        "(1,34): foster-parenting-character",
+        "(1,45): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "b": true,
+            "table": true,
+            "tbody": true,
+            "tr": true,
+            "td": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "b"
+                  },
+                  {
+                    "tag": "b",
+                    "children": [
+                      {
+                        "text": "bbb"
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "tbody",
+                        "children": [
+                          {
+                            "tag": "tr",
+                            "children": [
+                              {
+                                "tag": "td",
+                                "children": [
+                                  {
+                                    "text": "aaa"
+                                  }
+                                ]
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "b",
+                    "children": [
+                      {
+                        "text": "ccc"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><b></b><b>bbb</b><table><tbody><tr><td>aaa</td></tr></tbody></table><b>ccc</b></body></html>",
+        "noQuirksBodyHtml": "<b></b><b>bbb</b><table><tbody><tr><td>aaa</td></tr></tbody></table><b>ccc</b>"
+      }
+    },
+    {
+      "data": "A<table><tr> B</tr> B</table>",
+      "errors": [
+        "(1,1): expected-doctype-but-got-chars",
+        "(1,13): foster-parenting-character",
+        "(1,14): foster-parenting-character",
+        "(1,20): foster-parenting-character",
+        "(1,21): foster-parenting-character"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true,
+            "tbody": true,
+            "tr": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "A B B"
+                  },
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "tbody",
+                        "children": [
+                          {
+                            "tag": "tr"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>A B B<table><tbody><tr></tr></tbody></table></body></html>",
+        "noQuirksBodyHtml": "A B B<table><tbody><tr></tr></tbody></table>"
+      }
+    },
+    {
+      "data": "A<table><tr> B</tr> </em>C</table>",
+      "errors": [
+        "(1,1): expected-doctype-but-got-chars",
+        "(1,13): foster-parenting-character",
+        "(1,14): foster-parenting-character",
+        "(1,20): foster-parenting-character",
+        "(1,25): unexpected-end-tag",
+        "(1,25): unexpected-end-tag-in-special-element",
+        "(1,26): foster-parenting-character"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true,
+            "tbody": true,
+            "tr": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "A BC"
+                  },
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "tbody",
+                        "children": [
+                          {
+                            "tag": "tr"
+                          },
+                          {
+                            "text": " "
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>A BC<table><tbody><tr></tr> </tbody></table></body></html>",
+        "noQuirksBodyHtml": "A BC<table><tbody><tr></tr> </tbody></table>"
+      }
+    },
+    {
+      "data": "<select><keygen>",
+      "errors": [
+        "(1,8): expected-doctype-but-got-start-tag",
+        "(1,16): unexpected-input-in-select"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "select": true,
+            "keygen": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "select"
+                  },
+                  {
+                    "tag": "keygen"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><select></select><keygen></body></html>",
+        "noQuirksBodyHtml": "<select></select><keygen>"
+      }
+    }
+  ],
+  "tests8.dat": [
+    {
+      "data": "<div>\n<div></div>\n</span>x",
+      "errors": [
+        "(1,5): expected-doctype-but-got-start-tag",
+        "(3,7): unexpected-end-tag",
+        "(3,8): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "div": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "div",
+                    "children": [
+                      {
+                        "text": "\n"
+                      },
+                      {
+                        "tag": "div"
+                      },
+                      {
+                        "text": "\nx"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><div>\n<div></div>\nx</div></body></html>",
+        "noQuirksBodyHtml": "<div>\n<div></div>\nx</div>"
+      }
+    },
+    {
+      "data": "<div>x<div></div>\n</span>x",
+      "errors": [
+        "(1,5): expected-doctype-but-got-start-tag",
+        "(2,7): unexpected-end-tag",
+        "(2,8): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "div": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "div",
+                    "children": [
+                      {
+                        "text": "x"
+                      },
+                      {
+                        "tag": "div"
+                      },
+                      {
+                        "text": "\nx"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><div>x<div></div>\nx</div></body></html>",
+        "noQuirksBodyHtml": "<div>x<div></div>\nx</div>"
+      }
+    },
+    {
+      "data": "<div>x<div></div>x</span>x",
+      "errors": [
+        "(1,5): expected-doctype-but-got-start-tag",
+        "(1,25): unexpected-end-tag",
+        "(1,26): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "div": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "div",
+                    "children": [
+                      {
+                        "text": "x"
+                      },
+                      {
+                        "tag": "div"
+                      },
+                      {
+                        "text": "xx"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><div>x<div></div>xx</div></body></html>",
+        "noQuirksBodyHtml": "<div>x<div></div>xx</div>"
+      }
+    },
+    {
+      "data": "<div>x<div></div>y</span>z",
+      "errors": [
+        "(1,5): expected-doctype-but-got-start-tag",
+        "(1,25): unexpected-end-tag",
+        "(1,26): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "div": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "div",
+                    "children": [
+                      {
+                        "text": "x"
+                      },
+                      {
+                        "tag": "div"
+                      },
+                      {
+                        "text": "yz"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><div>x<div></div>yz</div></body></html>",
+        "noQuirksBodyHtml": "<div>x<div></div>yz</div>"
+      }
+    },
+    {
+      "data": "<table><div>x<div></div>x</span>x",
+      "errors": [
+        "(1,7): expected-doctype-but-got-start-tag",
+        "(1,12): foster-parenting-start-tag",
+        "(1,13): foster-parenting-character",
+        "(1,18): foster-parenting-start-tag",
+        "(1,24): foster-parenting-end-tag",
+        "(1,25): foster-parenting-start-tag",
+        "(1,32): foster-parenting-end-tag",
+        "(1,32): unexpected-end-tag",
+        "(1,33): foster-parenting-character",
+        "(1,33): eof-in-table"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "div": true,
+            "table": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "div",
+                    "children": [
+                      {
+                        "text": "x"
+                      },
+                      {
+                        "tag": "div"
+                      },
+                      {
+                        "text": "xx"
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "table"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><div>x<div></div>xx</div><table></table></body></html>",
+        "noQuirksBodyHtml": "<div>x<div></div>xx</div><table></table>"
+      }
+    },
+    {
+      "data": "x<table>x",
+      "errors": [
+        "(1,1): expected-doctype-but-got-chars",
+        "(1,9): foster-parenting-character",
+        "(1,9): eof-in-table"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "xx"
+                  },
+                  {
+                    "tag": "table"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>xx<table></table></body></html>",
+        "noQuirksBodyHtml": "xx<table></table>"
+      }
+    },
+    {
+      "data": "x<table><table>x",
+      "errors": [
+        "(1,1): expected-doctype-but-got-chars",
+        "(1,15): unexpected-start-tag-implies-end-tag",
+        "(1,16): foster-parenting-character",
+        "(1,16): eof-in-table"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "x"
+                  },
+                  {
+                    "tag": "table"
+                  },
+                  {
+                    "text": "x"
+                  },
+                  {
+                    "tag": "table"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>x<table></table>x<table></table></body></html>",
+        "noQuirksBodyHtml": "x<table></table>x<table></table>"
+      }
+    },
+    {
+      "data": "<b>a<div></div><div></b>y",
+      "errors": [
+        "(1,3): expected-doctype-but-got-start-tag",
+        "(1,24): adoption-agency-1.3",
+        "(1,25): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "b": true,
+            "div": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "b",
+                    "children": [
+                      {
+                        "text": "a"
+                      },
+                      {
+                        "tag": "div"
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "div",
+                    "children": [
+                      {
+                        "tag": "b"
+                      },
+                      {
+                        "text": "y"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><b>a<div></div></b><div><b></b>y</div></body></html>",
+        "noQuirksBodyHtml": "<b>a<div></div></b><div><b></b>y</div>"
+      }
+    },
+    {
+      "data": "<a><div><p></a>",
+      "errors": [
+        "(1,3): expected-doctype-but-got-start-tag",
+        "(1,15): adoption-agency-1.3",
+        "(1,15): adoption-agency-1.3",
+        "(1,15): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "a": true,
+            "div": true,
+            "p": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "a"
+                  },
+                  {
+                    "tag": "div",
+                    "children": [
+                      {
+                        "tag": "a"
+                      },
+                      {
+                        "tag": "p",
+                        "children": [
+                          {
+                            "tag": "a"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><a></a><div><a></a><p><a></a></p></div></body></html>",
+        "noQuirksBodyHtml": "<a></a><div><a></a><p><a></a></p></div>"
+      }
+    }
+  ],
+  "tests9.dat": [
+    {
+      "data": "<!DOCTYPE html><math></math>",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "math math": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "math",
+                    "ns": "http://www.w3.org/1998/Math/MathML"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><math></math></body></html>",
+        "noQuirksBodyHtml": "<math></math>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><body><math></math>",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "math math": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "math",
+                    "ns": "http://www.w3.org/1998/Math/MathML"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><math></math></body></html>",
+        "noQuirksBodyHtml": "<math></math>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><math><mi>",
+      "errors": [
+        "(1,25) expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "math math": true,
+            "math mi": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "math",
+                    "ns": "http://www.w3.org/1998/Math/MathML",
+                    "children": [
+                      {
+                        "tag": "mi",
+                        "ns": "http://www.w3.org/1998/Math/MathML"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><math><mi></mi></math></body></html>",
+        "noQuirksBodyHtml": "<math><mi></mi></math>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><math><annotation-xml><svg><u>",
+      "errors": [
+        "(1,45) unexpected-html-element-in-foreign-content",
+        "(1,45) expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "math math": true,
+            "math annotation-xml": true,
+            "svg svg": true,
+            "u": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "math",
+                    "ns": "http://www.w3.org/1998/Math/MathML",
+                    "children": [
+                      {
+                        "tag": "annotation-xml",
+                        "ns": "http://www.w3.org/1998/Math/MathML",
+                        "children": [
+                          {
+                            "tag": "svg",
+                            "ns": "http://www.w3.org/2000/svg"
+                          }
+                        ]
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "u"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><math><annotation-xml><svg></svg></annotation-xml></math><u></u></body></html>",
+        "noQuirksBodyHtml": "<math><annotation-xml><svg><u></u></svg></annotation-xml></math>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><body><select><math></math></select>",
+      "errors": [
+        "(1,35) unexpected-start-tag-in-select",
+        "(1,42) unexpected-end-tag-in-select"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "select": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "select"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><select></select></body></html>",
+        "noQuirksBodyHtml": "<select></select>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><body><select><option><math></math></option></select>",
+      "errors": [
+        "(1,43) unexpected-start-tag-in-select",
+        "(1,50) unexpected-end-tag-in-select"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "select": true,
+            "option": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "select",
+                    "children": [
+                      {
+                        "tag": "option"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><select><option></option></select></body></html>",
+        "noQuirksBodyHtml": "<select><option></option></select>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><body><table><math></math></table>",
+      "errors": [
+        "(1,34) unexpected-start-tag-implies-table-voodoo"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "math math": true,
+            "table": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "math",
+                    "ns": "http://www.w3.org/1998/Math/MathML"
+                  },
+                  {
+                    "tag": "table"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><math></math><table></table></body></html>",
+        "noQuirksBodyHtml": "<math></math><table></table>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><body><table><math><mi>foo</mi></math></table>",
+      "errors": [
+        "(1,34) foster-parenting-start-token",
+        "(1,39) foster-parenting-character",
+        "(1,40) foster-parenting-character",
+        "(1,41) foster-parenting-character"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "math math": true,
+            "math mi": true,
+            "table": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "math",
+                    "ns": "http://www.w3.org/1998/Math/MathML",
+                    "children": [
+                      {
+                        "tag": "mi",
+                        "ns": "http://www.w3.org/1998/Math/MathML",
+                        "children": [
+                          {
+                            "text": "foo"
+                          }
+                        ]
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "table"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><math><mi>foo</mi></math><table></table></body></html>",
+        "noQuirksBodyHtml": "<math><mi>foo</mi></math><table></table>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><body><table><math><mi>foo</mi><mi>bar</mi></math></table>",
+      "errors": [
+        "(1,34) foster-parenting-start-tag",
+        "(1,39) foster-parenting-character",
+        "(1,40) foster-parenting-character",
+        "(1,41) foster-parenting-character",
+        "(1,51) foster-parenting-character",
+        "(1,52) foster-parenting-character",
+        "(1,53) foster-parenting-character"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "math math": true,
+            "math mi": true,
+            "table": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "math",
+                    "ns": "http://www.w3.org/1998/Math/MathML",
+                    "children": [
+                      {
+                        "tag": "mi",
+                        "ns": "http://www.w3.org/1998/Math/MathML",
+                        "children": [
+                          {
+                            "text": "foo"
+                          }
+                        ]
+                      },
+                      {
+                        "tag": "mi",
+                        "ns": "http://www.w3.org/1998/Math/MathML",
+                        "children": [
+                          {
+                            "text": "bar"
+                          }
+                        ]
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "table"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><math><mi>foo</mi><mi>bar</mi></math><table></table></body></html>",
+        "noQuirksBodyHtml": "<math><mi>foo</mi><mi>bar</mi></math><table></table>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><body><table><tbody><math><mi>foo</mi><mi>bar</mi></math></tbody></table>",
+      "errors": [
+        "(1,41) foster-parenting-start-tag",
+        "(1,46) foster-parenting-character",
+        "(1,47) foster-parenting-character",
+        "(1,48) foster-parenting-character",
+        "(1,58) foster-parenting-character",
+        "(1,59) foster-parenting-character",
+        "(1,60) foster-parenting-character"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "math math": true,
+            "math mi": true,
+            "table": true,
+            "tbody": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "math",
+                    "ns": "http://www.w3.org/1998/Math/MathML",
+                    "children": [
+                      {
+                        "tag": "mi",
+                        "ns": "http://www.w3.org/1998/Math/MathML",
+                        "children": [
+                          {
+                            "text": "foo"
+                          }
+                        ]
+                      },
+                      {
+                        "tag": "mi",
+                        "ns": "http://www.w3.org/1998/Math/MathML",
+                        "children": [
+                          {
+                            "text": "bar"
+                          }
+                        ]
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "tbody"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><math><mi>foo</mi><mi>bar</mi></math><table><tbody></tbody></table></body></html>",
+        "noQuirksBodyHtml": "<math><mi>foo</mi><mi>bar</mi></math><table><tbody></tbody></table>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><body><table><tbody><tr><math><mi>foo</mi><mi>bar</mi></math></tr></tbody></table>",
+      "errors": [
+        "(1,45) foster-parenting-start-tag",
+        "(1,50) foster-parenting-character",
+        "(1,51) foster-parenting-character",
+        "(1,52) foster-parenting-character",
+        "(1,62) foster-parenting-character",
+        "(1,63) foster-parenting-character",
+        "(1,64) foster-parenting-character"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "math math": true,
+            "math mi": true,
+            "table": true,
+            "tbody": true,
+            "tr": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "math",
+                    "ns": "http://www.w3.org/1998/Math/MathML",
+                    "children": [
+                      {
+                        "tag": "mi",
+                        "ns": "http://www.w3.org/1998/Math/MathML",
+                        "children": [
+                          {
+                            "text": "foo"
+                          }
+                        ]
+                      },
+                      {
+                        "tag": "mi",
+                        "ns": "http://www.w3.org/1998/Math/MathML",
+                        "children": [
+                          {
+                            "text": "bar"
+                          }
+                        ]
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "tbody",
+                        "children": [
+                          {
+                            "tag": "tr"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><math><mi>foo</mi><mi>bar</mi></math><table><tbody><tr></tr></tbody></table></body></html>",
+        "noQuirksBodyHtml": "<math><mi>foo</mi><mi>bar</mi></math><table><tbody><tr></tr></tbody></table>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><body><table><tbody><tr><td><math><mi>foo</mi><mi>bar</mi></math></td></tr></tbody></table>",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true,
+            "tbody": true,
+            "tr": true,
+            "td": true,
+            "math math": true,
+            "math mi": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "tbody",
+                        "children": [
+                          {
+                            "tag": "tr",
+                            "children": [
+                              {
+                                "tag": "td",
+                                "children": [
+                                  {
+                                    "tag": "math",
+                                    "ns": "http://www.w3.org/1998/Math/MathML",
+                                    "children": [
+                                      {
+                                        "tag": "mi",
+                                        "ns": "http://www.w3.org/1998/Math/MathML",
+                                        "children": [
+                                          {
+                                            "text": "foo"
+                                          }
+                                        ]
+                                      },
+                                      {
+                                        "tag": "mi",
+                                        "ns": "http://www.w3.org/1998/Math/MathML",
+                                        "children": [
+                                          {
+                                            "text": "bar"
+                                          }
+                                        ]
+                                      }
+                                    ]
+                                  }
+                                ]
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><table><tbody><tr><td><math><mi>foo</mi><mi>bar</mi></math></td></tr></tbody></table></body></html>",
+        "noQuirksBodyHtml": "<table><tbody><tr><td><math><mi>foo</mi><mi>bar</mi></math></td></tr></tbody></table>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><body><table><tbody><tr><td><math><mi>foo</mi><mi>bar</mi></math><p>baz</td></tr></tbody></table>",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true,
+            "tbody": true,
+            "tr": true,
+            "td": true,
+            "math math": true,
+            "math mi": true,
+            "p": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "tbody",
+                        "children": [
+                          {
+                            "tag": "tr",
+                            "children": [
+                              {
+                                "tag": "td",
+                                "children": [
+                                  {
+                                    "tag": "math",
+                                    "ns": "http://www.w3.org/1998/Math/MathML",
+                                    "children": [
+                                      {
+                                        "tag": "mi",
+                                        "ns": "http://www.w3.org/1998/Math/MathML",
+                                        "children": [
+                                          {
+                                            "text": "foo"
+                                          }
+                                        ]
+                                      },
+                                      {
+                                        "tag": "mi",
+                                        "ns": "http://www.w3.org/1998/Math/MathML",
+                                        "children": [
+                                          {
+                                            "text": "bar"
+                                          }
+                                        ]
+                                      }
+                                    ]
+                                  },
+                                  {
+                                    "tag": "p",
+                                    "children": [
+                                      {
+                                        "text": "baz"
+                                      }
+                                    ]
+                                  }
+                                ]
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><table><tbody><tr><td><math><mi>foo</mi><mi>bar</mi></math><p>baz</p></td></tr></tbody></table></body></html>",
+        "noQuirksBodyHtml": "<table><tbody><tr><td><math><mi>foo</mi><mi>bar</mi></math><p>baz</p></td></tr></tbody></table>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><body><table><caption><math><mi>foo</mi><mi>bar</mi></math><p>baz</caption></table>",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true,
+            "caption": true,
+            "math math": true,
+            "math mi": true,
+            "p": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "caption",
+                        "children": [
+                          {
+                            "tag": "math",
+                            "ns": "http://www.w3.org/1998/Math/MathML",
+                            "children": [
+                              {
+                                "tag": "mi",
+                                "ns": "http://www.w3.org/1998/Math/MathML",
+                                "children": [
+                                  {
+                                    "text": "foo"
+                                  }
+                                ]
+                              },
+                              {
+                                "tag": "mi",
+                                "ns": "http://www.w3.org/1998/Math/MathML",
+                                "children": [
+                                  {
+                                    "text": "bar"
+                                  }
+                                ]
+                              }
+                            ]
+                          },
+                          {
+                            "tag": "p",
+                            "children": [
+                              {
+                                "text": "baz"
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><table><caption><math><mi>foo</mi><mi>bar</mi></math><p>baz</p></caption></table></body></html>",
+        "noQuirksBodyHtml": "<table><caption><math><mi>foo</mi><mi>bar</mi></math><p>baz</p></caption></table>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><body><table><caption><math><mi>foo</mi><mi>bar</mi><p>baz</table><p>quux",
+      "errors": [
+        "(1,70) unexpected-html-element-in-foreign-content"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true,
+            "caption": true,
+            "math math": true,
+            "math mi": true,
+            "p": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "caption",
+                        "children": [
+                          {
+                            "tag": "math",
+                            "ns": "http://www.w3.org/1998/Math/MathML",
+                            "children": [
+                              {
+                                "tag": "mi",
+                                "ns": "http://www.w3.org/1998/Math/MathML",
+                                "children": [
+                                  {
+                                    "text": "foo"
+                                  }
+                                ]
+                              },
+                              {
+                                "tag": "mi",
+                                "ns": "http://www.w3.org/1998/Math/MathML",
+                                "children": [
+                                  {
+                                    "text": "bar"
+                                  }
+                                ]
+                              }
+                            ]
+                          },
+                          {
+                            "tag": "p",
+                            "children": [
+                              {
+                                "text": "baz"
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "p",
+                    "children": [
+                      {
+                        "text": "quux"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><table><caption><math><mi>foo</mi><mi>bar</mi></math><p>baz</p></caption></table><p>quux</p></body></html>",
+        "noQuirksBodyHtml": "<table><caption><math><mi>foo</mi><mi>bar</mi><p>baz</p></math></caption></table><p>quux</p>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><body><table><caption><math><mi>foo</mi><mi>bar</mi>baz</table><p>quux",
+      "errors": [
+        "(1,78) unexpected-end-tag",
+        "(1,78) expected-one-end-tag-but-got-another"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true,
+            "caption": true,
+            "math math": true,
+            "math mi": true,
+            "p": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "caption",
+                        "children": [
+                          {
+                            "tag": "math",
+                            "ns": "http://www.w3.org/1998/Math/MathML",
+                            "children": [
+                              {
+                                "tag": "mi",
+                                "ns": "http://www.w3.org/1998/Math/MathML",
+                                "children": [
+                                  {
+                                    "text": "foo"
+                                  }
+                                ]
+                              },
+                              {
+                                "tag": "mi",
+                                "ns": "http://www.w3.org/1998/Math/MathML",
+                                "children": [
+                                  {
+                                    "text": "bar"
+                                  }
+                                ]
+                              },
+                              {
+                                "text": "baz"
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "p",
+                    "children": [
+                      {
+                        "text": "quux"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><table><caption><math><mi>foo</mi><mi>bar</mi>baz</math></caption></table><p>quux</p></body></html>",
+        "noQuirksBodyHtml": "<table><caption><math><mi>foo</mi><mi>bar</mi>baz</math></caption></table><p>quux</p>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><body><table><colgroup><math><mi>foo</mi><mi>bar</mi><p>baz</table><p>quux",
+      "errors": [
+        "(1,44) foster-parenting-start-tag",
+        "(1,49) foster-parenting-character",
+        "(1,50) foster-parenting-character",
+        "(1,51) foster-parenting-character",
+        "(1,61) foster-parenting-character",
+        "(1,62) foster-parenting-character",
+        "(1,63) foster-parenting-character",
+        "(1,71) unexpected-html-element-in-foreign-content",
+        "(1,71) foster-parenting-start-tag",
+        "(1,63) foster-parenting-character",
+        "(1,63) foster-parenting-character",
+        "(1,63) foster-parenting-character"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "math math": true,
+            "math mi": true,
+            "p": true,
+            "table": true,
+            "colgroup": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "math",
+                    "ns": "http://www.w3.org/1998/Math/MathML",
+                    "children": [
+                      {
+                        "tag": "mi",
+                        "ns": "http://www.w3.org/1998/Math/MathML",
+                        "children": [
+                          {
+                            "text": "foo"
+                          }
+                        ]
+                      },
+                      {
+                        "tag": "mi",
+                        "ns": "http://www.w3.org/1998/Math/MathML",
+                        "children": [
+                          {
+                            "text": "bar"
+                          }
+                        ]
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "p",
+                    "children": [
+                      {
+                        "text": "baz"
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "colgroup"
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "p",
+                    "children": [
+                      {
+                        "text": "quux"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><math><mi>foo</mi><mi>bar</mi></math><p>baz</p><table><colgroup></colgroup></table><p>quux</p></body></html>",
+        "noQuirksBodyHtml": "<math><mi>foo</mi><mi>bar</mi><p>baz</p></math><table><colgroup></colgroup></table><p>quux</p>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><body><table><tr><td><select><math><mi>foo</mi><mi>bar</mi><p>baz</table><p>quux",
+      "errors": [
+        "(1,50) unexpected-start-tag-in-select",
+        "(1,54) unexpected-start-tag-in-select",
+        "(1,62) unexpected-end-tag-in-select",
+        "(1,66) unexpected-start-tag-in-select",
+        "(1,74) unexpected-end-tag-in-select",
+        "(1,77) unexpected-start-tag-in-select",
+        "(1,88) unexpected-table-element-end-tag-in-select-in-table"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true,
+            "tbody": true,
+            "tr": true,
+            "td": true,
+            "select": true,
+            "p": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "tbody",
+                        "children": [
+                          {
+                            "tag": "tr",
+                            "children": [
+                              {
+                                "tag": "td",
+                                "children": [
+                                  {
+                                    "tag": "select",
+                                    "children": [
+                                      {
+                                        "text": "foobarbaz"
+                                      }
+                                    ]
+                                  }
+                                ]
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "p",
+                    "children": [
+                      {
+                        "text": "quux"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><table><tbody><tr><td><select>foobarbaz</select></td></tr></tbody></table><p>quux</p></body></html>",
+        "noQuirksBodyHtml": "<table><tbody><tr><td><select>foobarbaz</select></td></tr></tbody></table><p>quux</p>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><body><table><select><math><mi>foo</mi><mi>bar</mi><p>baz</table><p>quux",
+      "errors": [
+        "(1,36) unexpected-start-tag-implies-table-voodoo",
+        "(1,42) unexpected-start-tag-in-select",
+        "(1,46) unexpected-start-tag-in-select",
+        "(1,54) unexpected-end-tag-in-select",
+        "(1,58) unexpected-start-tag-in-select",
+        "(1,66) unexpected-end-tag-in-select",
+        "(1,69) unexpected-start-tag-in-select",
+        "(1,80) unexpected-table-element-end-tag-in-select-in-table"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "select": true,
+            "table": true,
+            "p": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "select",
+                    "children": [
+                      {
+                        "text": "foobarbaz"
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "table"
+                  },
+                  {
+                    "tag": "p",
+                    "children": [
+                      {
+                        "text": "quux"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><select>foobarbaz</select><table></table><p>quux</p></body></html>",
+        "noQuirksBodyHtml": "<select>foobarbaz</select><table></table><p>quux</p>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><body></body></html><math><mi>foo</mi><mi>bar</mi><p>baz",
+      "errors": [
+        "(1,41) expected-eof-but-got-start-tag",
+        "(1,68) unexpected-html-element-in-foreign-content"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "math math": true,
+            "math mi": true,
+            "p": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "math",
+                    "ns": "http://www.w3.org/1998/Math/MathML",
+                    "children": [
+                      {
+                        "tag": "mi",
+                        "ns": "http://www.w3.org/1998/Math/MathML",
+                        "children": [
+                          {
+                            "text": "foo"
+                          }
+                        ]
+                      },
+                      {
+                        "tag": "mi",
+                        "ns": "http://www.w3.org/1998/Math/MathML",
+                        "children": [
+                          {
+                            "text": "bar"
+                          }
+                        ]
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "p",
+                    "children": [
+                      {
+                        "text": "baz"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><math><mi>foo</mi><mi>bar</mi></math><p>baz</p></body></html>",
+        "noQuirksBodyHtml": "<math><mi>foo</mi><mi>bar</mi><p>baz</p></math>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><body></body><math><mi>foo</mi><mi>bar</mi><p>baz",
+      "errors": [
+        "(1,34) unexpected-start-tag-after-body",
+        "(1,61) unexpected-html-element-in-foreign-content"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "math math": true,
+            "math mi": true,
+            "p": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "math",
+                    "ns": "http://www.w3.org/1998/Math/MathML",
+                    "children": [
+                      {
+                        "tag": "mi",
+                        "ns": "http://www.w3.org/1998/Math/MathML",
+                        "children": [
+                          {
+                            "text": "foo"
+                          }
+                        ]
+                      },
+                      {
+                        "tag": "mi",
+                        "ns": "http://www.w3.org/1998/Math/MathML",
+                        "children": [
+                          {
+                            "text": "bar"
+                          }
+                        ]
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "p",
+                    "children": [
+                      {
+                        "text": "baz"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><math><mi>foo</mi><mi>bar</mi></math><p>baz</p></body></html>",
+        "noQuirksBodyHtml": "<math><mi>foo</mi><mi>bar</mi><p>baz</p></math>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><frameset><math><mi></mi><mi></mi><p><span>",
+      "errors": [
+        "(1,31) unexpected-start-tag-in-frameset",
+        "(1,35) unexpected-start-tag-in-frameset",
+        "(1,40) unexpected-end-tag-in-frameset",
+        "(1,44) unexpected-start-tag-in-frameset",
+        "(1,49) unexpected-end-tag-in-frameset",
+        "(1,52) unexpected-start-tag-in-frameset",
+        "(1,58) unexpected-start-tag-in-frameset",
+        "(1,58) eof-in-frameset"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "frameset": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "frameset"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><frameset></frameset></html>",
+        "noQuirksBodyHtml": "<math><mi></mi><mi></mi><p><span></span></p></math>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><frameset></frameset><math><mi></mi><mi></mi><p><span>",
+      "errors": [
+        "(1,42) unexpected-start-tag-after-frameset",
+        "(1,46) unexpected-start-tag-after-frameset",
+        "(1,51) unexpected-end-tag-after-frameset",
+        "(1,55) unexpected-start-tag-after-frameset",
+        "(1,60) unexpected-end-tag-after-frameset",
+        "(1,63) unexpected-start-tag-after-frameset",
+        "(1,69) unexpected-start-tag-after-frameset"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "frameset": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "frameset"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><frameset></frameset></html>",
+        "noQuirksBodyHtml": "<math><mi></mi><mi></mi><p><span></span></p></math>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><body xlink:href=foo><math xlink:href=foo></math>",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "math math": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "attrs": [
+                  {
+                    "name": "xlink:href",
+                    "value": "foo"
+                  }
+                ],
+                "children": [
+                  {
+                    "tag": "math",
+                    "ns": "http://www.w3.org/1998/Math/MathML",
+                    "attrs": [
+                      {
+                        "name": "href",
+                        "ns": "http://www.w3.org/1999/xlink",
+                        "value": "foo"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body xlink:href=\"foo\"><math xlink:href=\"foo\"></math></body></html>",
+        "noQuirksBodyHtml": "<math xlink:href=\"foo\"></math>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><body xlink:href=foo xml:lang=en><math><mi xml:lang=en xlink:href=foo></mi></math>",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "math math": true,
+            "math mi": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "attrs": [
+                  {
+                    "name": "xlink:href",
+                    "value": "foo"
+                  },
+                  {
+                    "name": "xml:lang",
+                    "value": "en"
+                  }
+                ],
+                "children": [
+                  {
+                    "tag": "math",
+                    "ns": "http://www.w3.org/1998/Math/MathML",
+                    "children": [
+                      {
+                        "tag": "mi",
+                        "ns": "http://www.w3.org/1998/Math/MathML",
+                        "attrs": [
+                          {
+                            "name": "href",
+                            "ns": "http://www.w3.org/1999/xlink",
+                            "value": "foo"
+                          },
+                          {
+                            "name": "lang",
+                            "ns": "http://www.w3.org/XML/1998/namespace",
+                            "value": "en"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body xlink:href=\"foo\" xml:lang=\"en\"><math><mi xml:lang=\"en\" xlink:href=\"foo\"></mi></math></body></html>",
+        "noQuirksBodyHtml": "<math><mi xml:lang=\"en\" xlink:href=\"foo\"></mi></math>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><body xlink:href=foo xml:lang=en><math><mi xml:lang=en xlink:href=foo /></math>",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "math math": true,
+            "math mi": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "attrs": [
+                  {
+                    "name": "xlink:href",
+                    "value": "foo"
+                  },
+                  {
+                    "name": "xml:lang",
+                    "value": "en"
+                  }
+                ],
+                "children": [
+                  {
+                    "tag": "math",
+                    "ns": "http://www.w3.org/1998/Math/MathML",
+                    "children": [
+                      {
+                        "tag": "mi",
+                        "ns": "http://www.w3.org/1998/Math/MathML",
+                        "attrs": [
+                          {
+                            "name": "href",
+                            "ns": "http://www.w3.org/1999/xlink",
+                            "value": "foo"
+                          },
+                          {
+                            "name": "lang",
+                            "ns": "http://www.w3.org/XML/1998/namespace",
+                            "value": "en"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body xlink:href=\"foo\" xml:lang=\"en\"><math><mi xml:lang=\"en\" xlink:href=\"foo\"></mi></math></body></html>",
+        "noQuirksBodyHtml": "<math><mi xml:lang=\"en\" xlink:href=\"foo\"></mi></math>"
+      }
+    },
+    {
+      "data": "<!DOCTYPE html><body xlink:href=foo xml:lang=en><math><mi xml:lang=en xlink:href=foo />bar</math>",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "math math": true,
+            "math mi": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "attrs": [
+                  {
+                    "name": "xlink:href",
+                    "value": "foo"
+                  },
+                  {
+                    "name": "xml:lang",
+                    "value": "en"
+                  }
+                ],
+                "children": [
+                  {
+                    "tag": "math",
+                    "ns": "http://www.w3.org/1998/Math/MathML",
+                    "children": [
+                      {
+                        "tag": "mi",
+                        "ns": "http://www.w3.org/1998/Math/MathML",
+                        "attrs": [
+                          {
+                            "name": "href",
+                            "ns": "http://www.w3.org/1999/xlink",
+                            "value": "foo"
+                          },
+                          {
+                            "name": "lang",
+                            "ns": "http://www.w3.org/XML/1998/namespace",
+                            "value": "en"
+                          }
+                        ]
+                      },
+                      {
+                        "text": "bar"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body xlink:href=\"foo\" xml:lang=\"en\"><math><mi xml:lang=\"en\" xlink:href=\"foo\"></mi>bar</math></body></html>",
+        "noQuirksBodyHtml": "<math><mi xml:lang=\"en\" xlink:href=\"foo\"></mi>bar</math>"
+      }
+    }
+  ],
+  "tests_innerHTML_1.dat": [
+    {
+      "data": "<body><span>",
+      "errors": [
+        "(1,6): unexpected-start-tag",
+        "(1,12): expected-closing-tag-but-got-eof"
+      ],
+      "fragment": {
+        "name": "body"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "span": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "span"
+          }
+        ],
+        "html": "<span></span>",
+        "noQuirksBodyHtml": "<span></span>"
+      }
+    },
+    {
+      "data": "<span><body>",
+      "errors": [
+        "(1,12): unexpected-start-tag",
+        "(1,12): expected-closing-tag-but-got-eof"
+      ],
+      "fragment": {
+        "name": "body"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "span": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "span"
+          }
+        ],
+        "html": "<span></span>",
+        "noQuirksBodyHtml": "<span></span>"
+      }
+    },
+    {
+      "data": "<span><body>",
+      "errors": [
+        "(1,12): unexpected-start-tag",
+        "(1,12): expected-closing-tag-but-got-eof"
+      ],
+      "fragment": {
+        "name": "div"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "span": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "span"
+          }
+        ],
+        "html": "<span></span>",
+        "noQuirksBodyHtml": "<span></span>"
+      }
+    },
+    {
+      "data": "<body><span>",
+      "errors": [
+        "(1,12): expected-closing-tag-but-got-eof"
+      ],
+      "fragment": {
+        "name": "html"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "head": true,
+            "body": true,
+            "span": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "head"
+          },
+          {
+            "tag": "body",
+            "children": [
+              {
+                "tag": "span"
+              }
+            ]
+          }
+        ],
+        "html": "<head></head><body><span></span></body>",
+        "noQuirksBodyHtml": "<span></span>"
+      }
+    },
+    {
+      "data": "<frameset><span>",
+      "errors": [
+        "(1,10): unexpected-start-tag",
+        "(1,16): expected-closing-tag-but-got-eof"
+      ],
+      "fragment": {
+        "name": "body"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "span": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "span"
+          }
+        ],
+        "html": "<span></span>",
+        "noQuirksBodyHtml": "<span></span>"
+      }
+    },
+    {
+      "data": "<span><frameset>",
+      "errors": [
+        "(1,16): unexpected-start-tag",
+        "(1,16): expected-closing-tag-but-got-eof"
+      ],
+      "fragment": {
+        "name": "body"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "span": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "span"
+          }
+        ],
+        "html": "<span></span>",
+        "noQuirksBodyHtml": "<span></span>"
+      }
+    },
+    {
+      "data": "<span><frameset>",
+      "errors": [
+        "(1,16): unexpected-start-tag",
+        "(1,16): expected-closing-tag-but-got-eof"
+      ],
+      "fragment": {
+        "name": "div"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "span": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "span"
+          }
+        ],
+        "html": "<span></span>",
+        "noQuirksBodyHtml": "<span></span>"
+      }
+    },
+    {
+      "data": "<frameset><span>",
+      "errors": [
+        "(1,16): unexpected-start-tag-in-frameset",
+        "(1,16): eof-in-frameset"
+      ],
+      "fragment": {
+        "name": "html"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "head": true,
+            "frameset": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "head"
+          },
+          {
+            "tag": "frameset"
+          }
+        ],
+        "html": "<head></head><frameset></frameset>",
+        "noQuirksBodyHtml": "<span></span>"
+      }
+    },
+    {
+      "data": "<table><tr>",
+      "errors": [
+        "(1,7): unexpected-start-tag"
+      ],
+      "fragment": {
+        "name": "table"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "tbody": true,
+            "tr": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "tbody",
+            "children": [
+              {
+                "tag": "tr"
+              }
+            ]
+          }
+        ],
+        "html": "<tbody><tr></tr></tbody>",
+        "noQuirksBodyHtml": "<table><tbody><tr></tr></tbody></table>"
+      }
+    },
+    {
+      "data": "</table><tr>",
+      "errors": [
+        "(1,8): unexpected-end-tag"
+      ],
+      "fragment": {
+        "name": "table"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "tbody": true,
+            "tr": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "tbody",
+            "children": [
+              {
+                "tag": "tr"
+              }
+            ]
+          }
+        ],
+        "html": "<tbody><tr></tr></tbody>",
+        "noQuirksBodyHtml": ""
+      }
+    },
+    {
+      "data": "<a>",
+      "errors": [
+        "(1,3): unexpected-start-tag-implies-table-voodoo",
+        "(1,3): eof-in-table"
+      ],
+      "fragment": {
+        "name": "table"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "a": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "a"
+          }
+        ],
+        "html": "<a></a>",
+        "noQuirksBodyHtml": "<a></a>"
+      }
+    },
+    {
+      "data": "<a>",
+      "errors": [
+        "(1,3): unexpected-start-tag-implies-table-voodoo",
+        "(1,3): eof-in-table"
+      ],
+      "fragment": {
+        "name": "table"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "a": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "a"
+          }
+        ],
+        "html": "<a></a>",
+        "noQuirksBodyHtml": "<a></a>"
+      }
+    },
+    {
+      "data": "<a><caption>a",
+      "errors": [
+        "(1,3): unexpected-start-tag-implies-table-voodoo",
+        "(1,13): expected-closing-tag-but-got-eof"
+      ],
+      "fragment": {
+        "name": "table"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "a": true,
+            "caption": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "a"
+          },
+          {
+            "tag": "caption",
+            "children": [
+              {
+                "text": "a"
+              }
+            ]
+          }
+        ],
+        "html": "<a></a><caption>a</caption>",
+        "noQuirksBodyHtml": "<a>a</a>"
+      }
+    },
+    {
+      "data": "<a><colgroup><col>",
+      "errors": [
+        "(1,3): foster-parenting-start-token",
+        "(1,18): expected-closing-tag-but-got-eof"
+      ],
+      "fragment": {
+        "name": "table"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "a": true,
+            "colgroup": true,
+            "col": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "a"
+          },
+          {
+            "tag": "colgroup",
+            "children": [
+              {
+                "tag": "col"
+              }
+            ]
+          }
+        ],
+        "html": "<a></a><colgroup><col></colgroup>",
+        "noQuirksBodyHtml": "<a></a>"
+      }
+    },
+    {
+      "data": "<a><tbody><tr>",
+      "errors": [
+        "(1,3): foster-parenting-start-tag"
+      ],
+      "fragment": {
+        "name": "table"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "a": true,
+            "tbody": true,
+            "tr": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "a"
+          },
+          {
+            "tag": "tbody",
+            "children": [
+              {
+                "tag": "tr"
+              }
+            ]
+          }
+        ],
+        "html": "<a></a><tbody><tr></tr></tbody>",
+        "noQuirksBodyHtml": "<a></a>"
+      }
+    },
+    {
+      "data": "<a><tfoot><tr>",
+      "errors": [
+        "(1,3): foster-parenting-start-tag"
+      ],
+      "fragment": {
+        "name": "table"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "a": true,
+            "tfoot": true,
+            "tr": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "a"
+          },
+          {
+            "tag": "tfoot",
+            "children": [
+              {
+                "tag": "tr"
+              }
+            ]
+          }
+        ],
+        "html": "<a></a><tfoot><tr></tr></tfoot>",
+        "noQuirksBodyHtml": "<a></a>"
+      }
+    },
+    {
+      "data": "<a><thead><tr>",
+      "errors": [
+        "(1,3): foster-parenting-start-tag"
+      ],
+      "fragment": {
+        "name": "table"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "a": true,
+            "thead": true,
+            "tr": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "a"
+          },
+          {
+            "tag": "thead",
+            "children": [
+              {
+                "tag": "tr"
+              }
+            ]
+          }
+        ],
+        "html": "<a></a><thead><tr></tr></thead>",
+        "noQuirksBodyHtml": "<a></a>"
+      }
+    },
+    {
+      "data": "<a><tr>",
+      "errors": [
+        "(1,3): foster-parenting-start-tag"
+      ],
+      "fragment": {
+        "name": "table"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "a": true,
+            "tbody": true,
+            "tr": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "a"
+          },
+          {
+            "tag": "tbody",
+            "children": [
+              {
+                "tag": "tr"
+              }
+            ]
+          }
+        ],
+        "html": "<a></a><tbody><tr></tr></tbody>",
+        "noQuirksBodyHtml": "<a></a>"
+      }
+    },
+    {
+      "data": "<a><th>",
+      "errors": [
+        "(1,3): unexpected-start-tag-implies-table-voodoo",
+        "(1,7): unexpected-cell-in-table-body"
+      ],
+      "fragment": {
+        "name": "table"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "a": true,
+            "tbody": true,
+            "tr": true,
+            "th": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "a"
+          },
+          {
+            "tag": "tbody",
+            "children": [
+              {
+                "tag": "tr",
+                "children": [
+                  {
+                    "tag": "th"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<a></a><tbody><tr><th></th></tr></tbody>",
+        "noQuirksBodyHtml": "<a></a>"
+      }
+    },
+    {
+      "data": "<a><td>",
+      "errors": [
+        "(1,3): unexpected-start-tag-implies-table-voodoo",
+        "(1,7): unexpected-cell-in-table-body"
+      ],
+      "fragment": {
+        "name": "table"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "a": true,
+            "tbody": true,
+            "tr": true,
+            "td": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "a"
+          },
+          {
+            "tag": "tbody",
+            "children": [
+              {
+                "tag": "tr",
+                "children": [
+                  {
+                    "tag": "td"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<a></a><tbody><tr><td></td></tr></tbody>",
+        "noQuirksBodyHtml": "<a></a>"
+      }
+    },
+    {
+      "data": "<table></table><tbody>",
+      "errors": [
+        "(1,22): unexpected-start-tag"
+      ],
+      "fragment": {
+        "name": "caption"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "table": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "table"
+          }
+        ],
+        "html": "<table></table>",
+        "noQuirksBodyHtml": "<table></table>"
+      }
+    },
+    {
+      "data": "</table><span>",
+      "errors": [
+        "(1,8): unexpected-end-tag",
+        "(1,14): expected-closing-tag-but-got-eof"
+      ],
+      "fragment": {
+        "name": "caption"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "span": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "span"
+          }
+        ],
+        "html": "<span></span>",
+        "noQuirksBodyHtml": "<span></span>"
+      }
+    },
+    {
+      "data": "<span></table>",
+      "errors": [
+        "(1,14): unexpected-end-tag",
+        "(1,14): expected-closing-tag-but-got-eof"
+      ],
+      "fragment": {
+        "name": "caption"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "span": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "span"
+          }
+        ],
+        "html": "<span></span>",
+        "noQuirksBodyHtml": "<span></span>"
+      }
+    },
+    {
+      "data": "</caption><span>",
+      "errors": [
+        "(1,10): XXX-undefined-error",
+        "(1,16): expected-closing-tag-but-got-eof"
+      ],
+      "fragment": {
+        "name": "caption"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "span": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "span"
+          }
+        ],
+        "html": "<span></span>",
+        "noQuirksBodyHtml": "<span></span>"
+      }
+    },
+    {
+      "data": "<span></caption><span>",
+      "errors": [
+        "(1,16): XXX-undefined-error",
+        "(1,22): expected-closing-tag-but-got-eof"
+      ],
+      "fragment": {
+        "name": "caption"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "span": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "span",
+            "children": [
+              {
+                "tag": "span"
+              }
+            ]
+          }
+        ],
+        "html": "<span><span></span></span>",
+        "noQuirksBodyHtml": "<span><span></span></span>"
+      }
+    },
+    {
+      "data": "<span><caption><span>",
+      "errors": [
+        "(1,15): unexpected-start-tag",
+        "(1,21): expected-closing-tag-but-got-eof"
+      ],
+      "fragment": {
+        "name": "caption"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "span": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "span",
+            "children": [
+              {
+                "tag": "span"
+              }
+            ]
+          }
+        ],
+        "html": "<span><span></span></span>",
+        "noQuirksBodyHtml": "<span><span></span></span>"
+      }
+    },
+    {
+      "data": "<span><col><span>",
+      "errors": [
+        "(1,11): unexpected-start-tag",
+        "(1,17): expected-closing-tag-but-got-eof"
+      ],
+      "fragment": {
+        "name": "caption"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "span": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "span",
+            "children": [
+              {
+                "tag": "span"
+              }
+            ]
+          }
+        ],
+        "html": "<span><span></span></span>",
+        "noQuirksBodyHtml": "<span><span></span></span>"
+      }
+    },
+    {
+      "data": "<span><colgroup><span>",
+      "errors": [
+        "(1,16): unexpected-start-tag",
+        "(1,22): expected-closing-tag-but-got-eof"
+      ],
+      "fragment": {
+        "name": "caption"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "span": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "span",
+            "children": [
+              {
+                "tag": "span"
+              }
+            ]
+          }
+        ],
+        "html": "<span><span></span></span>",
+        "noQuirksBodyHtml": "<span><span></span></span>"
+      }
+    },
+    {
+      "data": "<span><html><span>",
+      "errors": [
+        "(1,12): non-html-root",
+        "(1,18): expected-closing-tag-but-got-eof"
+      ],
+      "fragment": {
+        "name": "caption"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "span": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "span",
+            "children": [
+              {
+                "tag": "span"
+              }
+            ]
+          }
+        ],
+        "html": "<span><span></span></span>",
+        "noQuirksBodyHtml": "<span><span></span></span>"
+      }
+    },
+    {
+      "data": "<span><tbody><span>",
+      "errors": [
+        "(1,13): unexpected-start-tag",
+        "(1,19): expected-closing-tag-but-got-eof"
+      ],
+      "fragment": {
+        "name": "caption"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "span": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "span",
+            "children": [
+              {
+                "tag": "span"
+              }
+            ]
+          }
+        ],
+        "html": "<span><span></span></span>",
+        "noQuirksBodyHtml": "<span><span></span></span>"
+      }
+    },
+    {
+      "data": "<span><td><span>",
+      "errors": [
+        "(1,10): unexpected-start-tag",
+        "(1,16): expected-closing-tag-but-got-eof"
+      ],
+      "fragment": {
+        "name": "caption"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "span": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "span",
+            "children": [
+              {
+                "tag": "span"
+              }
+            ]
+          }
+        ],
+        "html": "<span><span></span></span>",
+        "noQuirksBodyHtml": "<span><span></span></span>"
+      }
+    },
+    {
+      "data": "<span><tfoot><span>",
+      "errors": [
+        "(1,13): unexpected-start-tag",
+        "(1,19): expected-closing-tag-but-got-eof"
+      ],
+      "fragment": {
+        "name": "caption"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "span": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "span",
+            "children": [
+              {
+                "tag": "span"
+              }
+            ]
+          }
+        ],
+        "html": "<span><span></span></span>",
+        "noQuirksBodyHtml": "<span><span></span></span>"
+      }
+    },
+    {
+      "data": "<span><thead><span>",
+      "errors": [
+        "(1,13): unexpected-start-tag",
+        "(1,19): expected-closing-tag-but-got-eof"
+      ],
+      "fragment": {
+        "name": "caption"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "span": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "span",
+            "children": [
+              {
+                "tag": "span"
+              }
+            ]
+          }
+        ],
+        "html": "<span><span></span></span>",
+        "noQuirksBodyHtml": "<span><span></span></span>"
+      }
+    },
+    {
+      "data": "<span><th><span>",
+      "errors": [
+        "(1,10): unexpected-start-tag",
+        "(1,16): expected-closing-tag-but-got-eof"
+      ],
+      "fragment": {
+        "name": "caption"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "span": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "span",
+            "children": [
+              {
+                "tag": "span"
+              }
+            ]
+          }
+        ],
+        "html": "<span><span></span></span>",
+        "noQuirksBodyHtml": "<span><span></span></span>"
+      }
+    },
+    {
+      "data": "<span><tr><span>",
+      "errors": [
+        "(1,10): unexpected-start-tag",
+        "(1,16): expected-closing-tag-but-got-eof"
+      ],
+      "fragment": {
+        "name": "caption"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "span": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "span",
+            "children": [
+              {
+                "tag": "span"
+              }
+            ]
+          }
+        ],
+        "html": "<span><span></span></span>",
+        "noQuirksBodyHtml": "<span><span></span></span>"
+      }
+    },
+    {
+      "data": "<span></table><span>",
+      "errors": [
+        "(1,14): unexpected-end-tag",
+        "(1,20): expected-closing-tag-but-got-eof"
+      ],
+      "fragment": {
+        "name": "caption"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "span": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "span",
+            "children": [
+              {
+                "tag": "span"
+              }
+            ]
+          }
+        ],
+        "html": "<span><span></span></span>",
+        "noQuirksBodyHtml": "<span><span></span></span>"
+      }
+    },
+    {
+      "data": "</colgroup><col>",
+      "errors": [
+        "(1,11): XXX-undefined-error"
+      ],
+      "fragment": {
+        "name": "colgroup"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "col": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "col"
+          }
+        ],
+        "html": "<col>",
+        "noQuirksBodyHtml": ""
+      }
+    },
+    {
+      "data": "<a><col>",
+      "errors": [
+        "(1,3): XXX-undefined-error"
+      ],
+      "fragment": {
+        "name": "colgroup"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "col": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "col"
+          }
+        ],
+        "html": "<col>",
+        "noQuirksBodyHtml": "<a></a>"
+      }
+    },
+    {
+      "data": "<caption><a>",
+      "errors": [
+        "(1,9): XXX-undefined-error",
+        "(1,12): unexpected-start-tag-implies-table-voodoo",
+        "(1,12): eof-in-table"
+      ],
+      "fragment": {
+        "name": "tbody"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "a": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "a"
+          }
+        ],
+        "html": "<a></a>",
+        "noQuirksBodyHtml": "<a></a>"
+      }
+    },
+    {
+      "data": "<col><a>",
+      "errors": [
+        "(1,5): XXX-undefined-error",
+        "(1,8): unexpected-start-tag-implies-table-voodoo",
+        "(1,8): eof-in-table"
+      ],
+      "fragment": {
+        "name": "tbody"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "a": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "a"
+          }
+        ],
+        "html": "<a></a>",
+        "noQuirksBodyHtml": "<a></a>"
+      }
+    },
+    {
+      "data": "<colgroup><a>",
+      "errors": [
+        "(1,10): XXX-undefined-error",
+        "(1,13): unexpected-start-tag-implies-table-voodoo",
+        "(1,13): eof-in-table"
+      ],
+      "fragment": {
+        "name": "tbody"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "a": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "a"
+          }
+        ],
+        "html": "<a></a>",
+        "noQuirksBodyHtml": "<a></a>"
+      }
+    },
+    {
+      "data": "<tbody><a>",
+      "errors": [
+        "(1,7): XXX-undefined-error",
+        "(1,10): unexpected-start-tag-implies-table-voodoo",
+        "(1,10): eof-in-table"
+      ],
+      "fragment": {
+        "name": "tbody"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "a": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "a"
+          }
+        ],
+        "html": "<a></a>",
+        "noQuirksBodyHtml": "<a></a>"
+      }
+    },
+    {
+      "data": "<tfoot><a>",
+      "errors": [
+        "(1,7): XXX-undefined-error",
+        "(1,10): unexpected-start-tag-implies-table-voodoo",
+        "(1,10): eof-in-table"
+      ],
+      "fragment": {
+        "name": "tbody"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "a": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "a"
+          }
+        ],
+        "html": "<a></a>",
+        "noQuirksBodyHtml": "<a></a>"
+      }
+    },
+    {
+      "data": "<thead><a>",
+      "errors": [
+        "(1,7): XXX-undefined-error",
+        "(1,10): unexpected-start-tag-implies-table-voodoo",
+        "(1,10): eof-in-table"
+      ],
+      "fragment": {
+        "name": "tbody"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "a": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "a"
+          }
+        ],
+        "html": "<a></a>",
+        "noQuirksBodyHtml": "<a></a>"
+      }
+    },
+    {
+      "data": "</table><a>",
+      "errors": [
+        "(1,8): XXX-undefined-error",
+        "(1,11): unexpected-start-tag-implies-table-voodoo",
+        "(1,11): eof-in-table"
+      ],
+      "fragment": {
+        "name": "tbody"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "a": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "a"
+          }
+        ],
+        "html": "<a></a>",
+        "noQuirksBodyHtml": "<a></a>"
+      }
+    },
+    {
+      "data": "<a><tr>",
+      "errors": [
+        "(1,3): unexpected-start-tag-implies-table-voodoo"
+      ],
+      "fragment": {
+        "name": "tbody"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "a": true,
+            "tr": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "a"
+          },
+          {
+            "tag": "tr"
+          }
+        ],
+        "html": "<a></a><tr></tr>",
+        "noQuirksBodyHtml": "<a></a>"
+      }
+    },
+    {
+      "data": "<a><td>",
+      "errors": [
+        "(1,3): unexpected-start-tag-implies-table-voodoo",
+        "(1,7): unexpected-cell-in-table-body"
+      ],
+      "fragment": {
+        "name": "tbody"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "a": true,
+            "tr": true,
+            "td": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "a"
+          },
+          {
+            "tag": "tr",
+            "children": [
+              {
+                "tag": "td"
+              }
+            ]
+          }
+        ],
+        "html": "<a></a><tr><td></td></tr>",
+        "noQuirksBodyHtml": "<a></a>"
+      }
+    },
+    {
+      "data": "<a><td>",
+      "errors": [
+        "(1,3): unexpected-start-tag-implies-table-voodoo",
+        "(1,7): unexpected-cell-in-table-body"
+      ],
+      "fragment": {
+        "name": "tbody"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "a": true,
+            "tr": true,
+            "td": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "a"
+          },
+          {
+            "tag": "tr",
+            "children": [
+              {
+                "tag": "td"
+              }
+            ]
+          }
+        ],
+        "html": "<a></a><tr><td></td></tr>",
+        "noQuirksBodyHtml": "<a></a>"
+      }
+    },
+    {
+      "data": "<a><td>",
+      "errors": [
+        "(1,3): unexpected-start-tag-implies-table-voodoo",
+        "(1,7): unexpected-cell-in-table-body"
+      ],
+      "fragment": {
+        "name": "tbody"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "a": true,
+            "tr": true,
+            "td": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "a"
+          },
+          {
+            "tag": "tr",
+            "children": [
+              {
+                "tag": "td"
+              }
+            ]
+          }
+        ],
+        "html": "<a></a><tr><td></td></tr>",
+        "noQuirksBodyHtml": "<a></a>"
+      }
+    },
+    {
+      "data": "<td><table><tbody><a><tr>",
+      "errors": [
+        "(1,4): unexpected-cell-in-table-body",
+        "(1,21): unexpected-start-tag-implies-table-voodoo",
+        "(1,25): eof-in-table"
+      ],
+      "fragment": {
+        "name": "tbody"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "tr": true,
+            "td": true,
+            "a": true,
+            "table": true,
+            "tbody": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "tr",
+            "children": [
+              {
+                "tag": "td",
+                "children": [
+                  {
+                    "tag": "a"
+                  },
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "tbody",
+                        "children": [
+                          {
+                            "tag": "tr"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<tr><td><a></a><table><tbody><tr></tr></tbody></table></td></tr>",
+        "noQuirksBodyHtml": "<a></a><table><tbody><tr></tr></tbody></table>"
+      }
+    },
+    {
+      "data": "</tr><td>",
+      "errors": [
+        "(1,5): XXX-undefined-error"
+      ],
+      "fragment": {
+        "name": "tr"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "td": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "td"
+          }
+        ],
+        "html": "<td></td>",
+        "noQuirksBodyHtml": ""
+      }
+    },
+    {
+      "data": "<td><table><a><tr></tr><tr>",
+      "errors": [
+        "(1,14): unexpected-start-tag-implies-table-voodoo",
+        "(1,27): eof-in-table"
+      ],
+      "fragment": {
+        "name": "tr"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "td": true,
+            "a": true,
+            "table": true,
+            "tbody": true,
+            "tr": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "td",
+            "children": [
+              {
+                "tag": "a"
+              },
+              {
+                "tag": "table",
+                "children": [
+                  {
+                    "tag": "tbody",
+                    "children": [
+                      {
+                        "tag": "tr"
+                      },
+                      {
+                        "tag": "tr"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<td><a></a><table><tbody><tr></tr><tr></tr></tbody></table></td>",
+        "noQuirksBodyHtml": "<a></a><table><tbody><tr></tr><tr></tr></tbody></table>"
+      }
+    },
+    {
+      "data": "<caption><td>",
+      "errors": [
+        "(1,9): XXX-undefined-error"
+      ],
+      "fragment": {
+        "name": "tr"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "td": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "td"
+          }
+        ],
+        "html": "<td></td>",
+        "noQuirksBodyHtml": ""
+      }
+    },
+    {
+      "data": "<col><td>",
+      "errors": [
+        "(1,5): XXX-undefined-error"
+      ],
+      "fragment": {
+        "name": "tr"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "td": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "td"
+          }
+        ],
+        "html": "<td></td>",
+        "noQuirksBodyHtml": ""
+      }
+    },
+    {
+      "data": "<colgroup><td>",
+      "errors": [
+        "(1,10): XXX-undefined-error"
+      ],
+      "fragment": {
+        "name": "tr"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "td": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "td"
+          }
+        ],
+        "html": "<td></td>",
+        "noQuirksBodyHtml": ""
+      }
+    },
+    {
+      "data": "<tbody><td>",
+      "errors": [
+        "(1,7): XXX-undefined-error"
+      ],
+      "fragment": {
+        "name": "tr"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "td": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "td"
+          }
+        ],
+        "html": "<td></td>",
+        "noQuirksBodyHtml": ""
+      }
+    },
+    {
+      "data": "<tfoot><td>",
+      "errors": [
+        "(1,7): XXX-undefined-error"
+      ],
+      "fragment": {
+        "name": "tr"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "td": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "td"
+          }
+        ],
+        "html": "<td></td>",
+        "noQuirksBodyHtml": ""
+      }
+    },
+    {
+      "data": "<thead><td>",
+      "errors": [
+        "(1,7): XXX-undefined-error"
+      ],
+      "fragment": {
+        "name": "tr"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "td": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "td"
+          }
+        ],
+        "html": "<td></td>",
+        "noQuirksBodyHtml": ""
+      }
+    },
+    {
+      "data": "<tr><td>",
+      "errors": [
+        "(1,4): XXX-undefined-error"
+      ],
+      "fragment": {
+        "name": "tr"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "td": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "td"
+          }
+        ],
+        "html": "<td></td>",
+        "noQuirksBodyHtml": ""
+      }
+    },
+    {
+      "data": "</table><td>",
+      "errors": [
+        "(1,8): XXX-undefined-error"
+      ],
+      "fragment": {
+        "name": "tr"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "td": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "td"
+          }
+        ],
+        "html": "<td></td>",
+        "noQuirksBodyHtml": ""
+      }
+    },
+    {
+      "data": "<td><table></table><td>",
+      "errors": [],
+      "fragment": {
+        "name": "tr"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "td": true,
+            "table": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "td",
+            "children": [
+              {
+                "tag": "table"
+              }
+            ]
+          },
+          {
+            "tag": "td"
+          }
+        ],
+        "html": "<td><table></table></td><td></td>",
+        "noQuirksBodyHtml": "<table></table>"
+      }
+    },
+    {
+      "data": "<td><table></table><td>",
+      "errors": [],
+      "fragment": {
+        "name": "tr"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "td": true,
+            "table": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "td",
+            "children": [
+              {
+                "tag": "table"
+              }
+            ]
+          },
+          {
+            "tag": "td"
+          }
+        ],
+        "html": "<td><table></table></td><td></td>",
+        "noQuirksBodyHtml": "<table></table>"
+      }
+    },
+    {
+      "data": "<caption><a>",
+      "errors": [
+        "(1,9): XXX-undefined-error",
+        "(1,12): expected-closing-tag-but-got-eof"
+      ],
+      "fragment": {
+        "name": "td"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "a": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "a"
+          }
+        ],
+        "html": "<a></a>",
+        "noQuirksBodyHtml": "<a></a>"
+      }
+    },
+    {
+      "data": "<col><a>",
+      "errors": [
+        "(1,5): XXX-undefined-error",
+        "(1,8): expected-closing-tag-but-got-eof"
+      ],
+      "fragment": {
+        "name": "td"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "a": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "a"
+          }
+        ],
+        "html": "<a></a>",
+        "noQuirksBodyHtml": "<a></a>"
+      }
+    },
+    {
+      "data": "<colgroup><a>",
+      "errors": [
+        "(1,10): XXX-undefined-error",
+        "(1,13): expected-closing-tag-but-got-eof"
+      ],
+      "fragment": {
+        "name": "td"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "a": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "a"
+          }
+        ],
+        "html": "<a></a>",
+        "noQuirksBodyHtml": "<a></a>"
+      }
+    },
+    {
+      "data": "<tbody><a>",
+      "errors": [
+        "(1,7): XXX-undefined-error",
+        "(1,10): expected-closing-tag-but-got-eof"
+      ],
+      "fragment": {
+        "name": "td"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "a": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "a"
+          }
+        ],
+        "html": "<a></a>",
+        "noQuirksBodyHtml": "<a></a>"
+      }
+    },
+    {
+      "data": "<tfoot><a>",
+      "errors": [
+        "(1,7): XXX-undefined-error",
+        "(1,10): expected-closing-tag-but-got-eof"
+      ],
+      "fragment": {
+        "name": "td"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "a": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "a"
+          }
+        ],
+        "html": "<a></a>",
+        "noQuirksBodyHtml": "<a></a>"
+      }
+    },
+    {
+      "data": "<th><a>",
+      "errors": [
+        "(1,4): XXX-undefined-error",
+        "(1,7): expected-closing-tag-but-got-eof"
+      ],
+      "fragment": {
+        "name": "td"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "a": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "a"
+          }
+        ],
+        "html": "<a></a>",
+        "noQuirksBodyHtml": "<a></a>"
+      }
+    },
+    {
+      "data": "<thead><a>",
+      "errors": [
+        "(1,7): XXX-undefined-error",
+        "(1,10): expected-closing-tag-but-got-eof"
+      ],
+      "fragment": {
+        "name": "td"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "a": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "a"
+          }
+        ],
+        "html": "<a></a>",
+        "noQuirksBodyHtml": "<a></a>"
+      }
+    },
+    {
+      "data": "<tr><a>",
+      "errors": [
+        "(1,4): XXX-undefined-error",
+        "(1,7): expected-closing-tag-but-got-eof"
+      ],
+      "fragment": {
+        "name": "td"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "a": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "a"
+          }
+        ],
+        "html": "<a></a>",
+        "noQuirksBodyHtml": "<a></a>"
+      }
+    },
+    {
+      "data": "</table><a>",
+      "errors": [
+        "(1,8): XXX-undefined-error",
+        "(1,11): expected-closing-tag-but-got-eof"
+      ],
+      "fragment": {
+        "name": "td"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "a": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "a"
+          }
+        ],
+        "html": "<a></a>",
+        "noQuirksBodyHtml": "<a></a>"
+      }
+    },
+    {
+      "data": "</tbody><a>",
+      "errors": [
+        "(1,8): XXX-undefined-error",
+        "(1,11): expected-closing-tag-but-got-eof"
+      ],
+      "fragment": {
+        "name": "td"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "a": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "a"
+          }
+        ],
+        "html": "<a></a>",
+        "noQuirksBodyHtml": "<a></a>"
+      }
+    },
+    {
+      "data": "</td><a>",
+      "errors": [
+        "(1,5): unexpected-end-tag",
+        "(1,8): expected-closing-tag-but-got-eof"
+      ],
+      "fragment": {
+        "name": "td"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "a": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "a"
+          }
+        ],
+        "html": "<a></a>",
+        "noQuirksBodyHtml": "<a></a>"
+      }
+    },
+    {
+      "data": "</tfoot><a>",
+      "errors": [
+        "(1,8): XXX-undefined-error",
+        "(1,11): expected-closing-tag-but-got-eof"
+      ],
+      "fragment": {
+        "name": "td"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "a": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "a"
+          }
+        ],
+        "html": "<a></a>",
+        "noQuirksBodyHtml": "<a></a>"
+      }
+    },
+    {
+      "data": "</thead><a>",
+      "errors": [
+        "(1,8): XXX-undefined-error",
+        "(1,11): expected-closing-tag-but-got-eof"
+      ],
+      "fragment": {
+        "name": "td"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "a": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "a"
+          }
+        ],
+        "html": "<a></a>",
+        "noQuirksBodyHtml": "<a></a>"
+      }
+    },
+    {
+      "data": "</th><a>",
+      "errors": [
+        "(1,5): unexpected-end-tag",
+        "(1,8): expected-closing-tag-but-got-eof"
+      ],
+      "fragment": {
+        "name": "td"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "a": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "a"
+          }
+        ],
+        "html": "<a></a>",
+        "noQuirksBodyHtml": "<a></a>"
+      }
+    },
+    {
+      "data": "</tr><a>",
+      "errors": [
+        "(1,5): XXX-undefined-error",
+        "(1,8): expected-closing-tag-but-got-eof"
+      ],
+      "fragment": {
+        "name": "td"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "a": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "a"
+          }
+        ],
+        "html": "<a></a>",
+        "noQuirksBodyHtml": "<a></a>"
+      }
+    },
+    {
+      "data": "<table><td><td>",
+      "errors": [
+        "(1,11): unexpected-cell-in-table-body",
+        "(1,15): expected-closing-tag-but-got-eof"
+      ],
+      "fragment": {
+        "name": "td"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "table": true,
+            "tbody": true,
+            "tr": true,
+            "td": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "table",
+            "children": [
+              {
+                "tag": "tbody",
+                "children": [
+                  {
+                    "tag": "tr",
+                    "children": [
+                      {
+                        "tag": "td"
+                      },
+                      {
+                        "tag": "td"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<table><tbody><tr><td></td><td></td></tr></tbody></table>",
+        "noQuirksBodyHtml": "<table><tbody><tr><td></td><td></td></tr></tbody></table>"
+      }
+    },
+    {
+      "data": "</select><option>",
+      "errors": [
+        "(1,9): XXX-undefined-error"
+      ],
+      "fragment": {
+        "name": "select"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "option": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "option"
+          }
+        ],
+        "html": "<option></option>",
+        "noQuirksBodyHtml": "<option></option>"
+      }
+    },
+    {
+      "data": "<input><option>",
+      "errors": [
+        "(1,7): unexpected-input-in-select"
+      ],
+      "fragment": {
+        "name": "select"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "option": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "option"
+          }
+        ],
+        "html": "<option></option>",
+        "noQuirksBodyHtml": "<input><option></option>"
+      }
+    },
+    {
+      "data": "<keygen><option>",
+      "errors": [
+        "(1,8): unexpected-input-in-select"
+      ],
+      "fragment": {
+        "name": "select"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "option": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "option"
+          }
+        ],
+        "html": "<option></option>",
+        "noQuirksBodyHtml": "<keygen><option></option>"
+      }
+    },
+    {
+      "data": "<textarea><option>",
+      "errors": [
+        "(1,10): unexpected-input-in-select"
+      ],
+      "fragment": {
+        "name": "select"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "option": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "option"
+          }
+        ],
+        "html": "<option></option>",
+        "noQuirksBodyHtml": "<textarea>&lt;option&gt;</textarea>"
+      }
+    },
+    {
+      "data": "</html><!--abc-->",
+      "errors": [
+        "(1,7): unexpected-end-tag-after-body-innerhtml"
+      ],
+      "fragment": {
+        "name": "html"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "head": true,
+            "body": true
+          },
+          "comment": true
+        },
+        "tree": [
+          {
+            "tag": "head"
+          },
+          {
+            "tag": "body"
+          },
+          {
+            "comment": "abc"
+          }
+        ],
+        "html": "<head></head><body></body><!--abc-->",
+        "noQuirksBodyHtml": "<!--abc-->"
+      }
+    },
+    {
+      "data": "</frameset><frame>",
+      "errors": [
+        "(1,11): unexpected-frameset-in-frameset-innerhtml"
+      ],
+      "fragment": {
+        "name": "frameset"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "frame": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "frame"
+          }
+        ],
+        "html": "<frame>",
+        "noQuirksBodyHtml": ""
+      }
+    },
+    {
+      "data": "",
+      "errors": [],
+      "fragment": {
+        "name": "html"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "head": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "head"
+          },
+          {
+            "tag": "body"
+          }
+        ],
+        "html": "<head></head><body></body>",
+        "noQuirksBodyHtml": ""
+      }
+    }
+  ],
+  "tricky01.dat": [
+    {
+      "data": "<b><p>Bold </b> Not bold</p>\nAlso not bold.",
+      "errors": [
+        "(1,3): expected-doctype-but-got-start-tag",
+        "(1,15): adoption-agency-1.3"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "b": true,
+            "p": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "b"
+                  },
+                  {
+                    "tag": "p",
+                    "children": [
+                      {
+                        "tag": "b",
+                        "children": [
+                          {
+                            "text": "Bold "
+                          }
+                        ]
+                      },
+                      {
+                        "text": " Not bold"
+                      }
+                    ]
+                  },
+                  {
+                    "text": "\nAlso not bold."
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><b></b><p><b>Bold </b> Not bold</p>\nAlso not bold.</body></html>",
+        "noQuirksBodyHtml": "<b></b><p><b>Bold </b> Not bold</p>\nAlso not bold."
+      }
+    },
+    {
+      "data": "<html>\n<font color=red><i>Italic and Red<p>Italic and Red </font> Just italic.</p> Italic only.</i> Plain\n<p>I should not be red. <font color=red>Red. <i>Italic and red.</p>\n<p>Italic and red. </i> Red.</font> I should not be red.</p>\n<b>Bold <i>Bold and italic</b> Only Italic </i> Plain",
+      "errors": [
+        "(1,6): expected-doctype-but-got-start-tag",
+        "(2,58): adoption-agency-1.3",
+        "(3,67): unexpected-end-tag",
+        "(4,23): adoption-agency-1.3",
+        "(4,35): adoption-agency-1.3",
+        "(5,30): adoption-agency-1.3"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "font": true,
+            "i": true,
+            "p": true,
+            "b": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "font",
+                    "attrs": [
+                      {
+                        "name": "color",
+                        "value": "red"
+                      }
+                    ],
+                    "children": [
+                      {
+                        "tag": "i",
+                        "children": [
+                          {
+                            "text": "Italic and Red"
+                          }
+                        ]
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "i",
+                    "children": [
+                      {
+                        "tag": "p",
+                        "children": [
+                          {
+                            "tag": "font",
+                            "attrs": [
+                              {
+                                "name": "color",
+                                "value": "red"
+                              }
+                            ],
+                            "children": [
+                              {
+                                "text": "Italic and Red "
+                              }
+                            ]
+                          },
+                          {
+                            "text": " Just italic."
+                          }
+                        ]
+                      },
+                      {
+                        "text": " Italic only."
+                      }
+                    ]
+                  },
+                  {
+                    "text": " Plain\n"
+                  },
+                  {
+                    "tag": "p",
+                    "children": [
+                      {
+                        "text": "I should not be red. "
+                      },
+                      {
+                        "tag": "font",
+                        "attrs": [
+                          {
+                            "name": "color",
+                            "value": "red"
+                          }
+                        ],
+                        "children": [
+                          {
+                            "text": "Red. "
+                          },
+                          {
+                            "tag": "i",
+                            "children": [
+                              {
+                                "text": "Italic and red."
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "font",
+                    "attrs": [
+                      {
+                        "name": "color",
+                        "value": "red"
+                      }
+                    ],
+                    "children": [
+                      {
+                        "tag": "i",
+                        "children": [
+                          {
+                            "text": "\n"
+                          }
+                        ]
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "p",
+                    "children": [
+                      {
+                        "tag": "font",
+                        "attrs": [
+                          {
+                            "name": "color",
+                            "value": "red"
+                          }
+                        ],
+                        "children": [
+                          {
+                            "tag": "i",
+                            "children": [
+                              {
+                                "text": "Italic and red. "
+                              }
+                            ]
+                          },
+                          {
+                            "text": " Red."
+                          }
+                        ]
+                      },
+                      {
+                        "text": " I should not be red."
+                      }
+                    ]
+                  },
+                  {
+                    "text": "\n"
+                  },
+                  {
+                    "tag": "b",
+                    "children": [
+                      {
+                        "text": "Bold "
+                      },
+                      {
+                        "tag": "i",
+                        "children": [
+                          {
+                            "text": "Bold and italic"
+                          }
+                        ]
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "i",
+                    "children": [
+                      {
+                        "text": " Only Italic "
+                      }
+                    ]
+                  },
+                  {
+                    "text": " Plain"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><font color=\"red\"><i>Italic and Red</i></font><i><p><font color=\"red\">Italic and Red </font> Just italic.</p> Italic only.</i> Plain\n<p>I should not be red. <font color=\"red\">Red. <i>Italic and red.</i></font></p><font color=\"red\"><i>\n</i></font><p><font color=\"red\"><i>Italic and red. </i> Red.</font> I should not be red.</p>\n<b>Bold <i>Bold and italic</i></b><i> Only Italic </i> Plain</body></html>",
+        "noQuirksBodyHtml": "\n<font color=\"red\"><i>Italic and Red</i></font><i><p><font color=\"red\">Italic and Red </font> Just italic.</p> Italic only.</i> Plain\n<p>I should not be red. <font color=\"red\">Red. <i>Italic and red.</i></font></p><font color=\"red\"><i>\n</i></font><p><font color=\"red\"><i>Italic and red. </i> Red.</font> I should not be red.</p>\n<b>Bold <i>Bold and italic</i></b><i> Only Italic </i> Plain"
+      }
+    },
+    {
+      "data": "<html><body>\n<p><font size=\"7\">First paragraph.</p>\n<p>Second paragraph.</p></font>\n<b><p><i>Bold and Italic</b> Italic</p>",
+      "errors": [
+        "(1,6): expected-doctype-but-got-start-tag",
+        "(2,38): unexpected-end-tag",
+        "(4,28): adoption-agency-1.3",
+        "(4,28): adoption-agency-1.3",
+        "(4,39): unexpected-end-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "p": true,
+            "font": true,
+            "b": true,
+            "i": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "\n"
+                  },
+                  {
+                    "tag": "p",
+                    "children": [
+                      {
+                        "tag": "font",
+                        "attrs": [
+                          {
+                            "name": "size",
+                            "value": "7"
+                          }
+                        ],
+                        "children": [
+                          {
+                            "text": "First paragraph."
+                          }
+                        ]
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "font",
+                    "attrs": [
+                      {
+                        "name": "size",
+                        "value": "7"
+                      }
+                    ],
+                    "children": [
+                      {
+                        "text": "\n"
+                      },
+                      {
+                        "tag": "p",
+                        "children": [
+                          {
+                            "text": "Second paragraph."
+                          }
+                        ]
+                      }
+                    ]
+                  },
+                  {
+                    "text": "\n"
+                  },
+                  {
+                    "tag": "b"
+                  },
+                  {
+                    "tag": "p",
+                    "children": [
+                      {
+                        "tag": "b",
+                        "children": [
+                          {
+                            "tag": "i",
+                            "children": [
+                              {
+                                "text": "Bold and Italic"
+                              }
+                            ]
+                          }
+                        ]
+                      },
+                      {
+                        "tag": "i",
+                        "children": [
+                          {
+                            "text": " Italic"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>\n<p><font size=\"7\">First paragraph.</font></p><font size=\"7\">\n<p>Second paragraph.</p></font>\n<b></b><p><b><i>Bold and Italic</i></b><i> Italic</i></p></body></html>",
+        "noQuirksBodyHtml": "\n<p><font size=\"7\">First paragraph.</font></p><font size=\"7\">\n<p>Second paragraph.</p></font>\n<b></b><p><b><i>Bold and Italic</i></b><i> Italic</i></p>"
+      }
+    },
+    {
+      "data": "<html>\n<dl>\n<dt><b>Boo\n<dd>Goo?\n</dl>\n</html>",
+      "errors": [
+        "(1,6): expected-doctype-but-got-start-tag",
+        "(4,4): end-tag-too-early",
+        "(5,5): end-tag-too-early",
+        "(6,7): expected-one-end-tag-but-got-another"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "dl": true,
+            "dt": true,
+            "b": true,
+            "dd": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "dl",
+                    "children": [
+                      {
+                        "text": "\n"
+                      },
+                      {
+                        "tag": "dt",
+                        "children": [
+                          {
+                            "tag": "b",
+                            "children": [
+                              {
+                                "text": "Boo\n"
+                              }
+                            ]
+                          }
+                        ]
+                      },
+                      {
+                        "tag": "dd",
+                        "children": [
+                          {
+                            "tag": "b",
+                            "children": [
+                              {
+                                "text": "Goo?\n"
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "b",
+                    "children": [
+                      {
+                        "text": "\n"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><dl>\n<dt><b>Boo\n</b></dt><dd><b>Goo?\n</b></dd></dl><b>\n</b></body></html>",
+        "noQuirksBodyHtml": "\n<dl>\n<dt><b>Boo\n</b></dt><dd><b>Goo?\n</b></dd></dl><b>\n</b>"
+      }
+    },
+    {
+      "data": "<html><body>\n<label><a><div>Hello<div>World</div></a></label>  \n</body></html>",
+      "errors": [
+        "(1,6): expected-doctype-but-got-start-tag",
+        "(2,40): adoption-agency-1.3",
+        "(2,48): unexpected-end-tag",
+        "(3,7): expected-one-end-tag-but-got-another"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "label": true,
+            "a": true,
+            "div": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "\n"
+                  },
+                  {
+                    "tag": "label",
+                    "children": [
+                      {
+                        "tag": "a"
+                      },
+                      {
+                        "tag": "div",
+                        "children": [
+                          {
+                            "tag": "a",
+                            "children": [
+                              {
+                                "text": "Hello"
+                              },
+                              {
+                                "tag": "div",
+                                "children": [
+                                  {
+                                    "text": "World"
+                                  }
+                                ]
+                              }
+                            ]
+                          },
+                          {
+                            "text": "  \n"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>\n<label><a></a><div><a>Hello<div>World</div></a>  \n</div></label></body></html>",
+        "noQuirksBodyHtml": "\n<label><a></a><div><a>Hello<div>World</div></a>  \n</div></label>"
+      }
+    },
+    {
+      "data": "<table><center> <font>a</center> <img> <tr><td> </td> </tr> </table>",
+      "errors": [
+        "(1,7): expected-doctype-but-got-start-tag",
+        "(1,15): foster-parenting-start-tag",
+        "(1,16): foster-parenting-character",
+        "(1,22): foster-parenting-start-tag",
+        "(1,23): foster-parenting-character",
+        "(1,32): foster-parenting-end-tag",
+        "(1,32): end-tag-too-early",
+        "(1,33): foster-parenting-character",
+        "(1,38): foster-parenting-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "center": true,
+            "font": true,
+            "img": true,
+            "table": true,
+            "tbody": true,
+            "tr": true,
+            "td": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "center",
+                    "children": [
+                      {
+                        "text": " "
+                      },
+                      {
+                        "tag": "font",
+                        "children": [
+                          {
+                            "text": "a"
+                          }
+                        ]
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "font",
+                    "children": [
+                      {
+                        "tag": "img"
+                      },
+                      {
+                        "text": " "
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "text": " "
+                      },
+                      {
+                        "tag": "tbody",
+                        "children": [
+                          {
+                            "tag": "tr",
+                            "children": [
+                              {
+                                "tag": "td",
+                                "children": [
+                                  {
+                                    "text": " "
+                                  }
+                                ]
+                              },
+                              {
+                                "text": " "
+                              }
+                            ]
+                          },
+                          {
+                            "text": " "
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><center> <font>a</font></center><font><img> </font><table> <tbody><tr><td> </td> </tr> </tbody></table></body></html>",
+        "noQuirksBodyHtml": "<center> <font>a</font></center><font><img> </font><table> <tbody><tr><td> </td> </tr> </tbody></table>"
+      }
+    },
+    {
+      "data": "<table><tr><p><a><p>You should see this text.",
+      "errors": [
+        "(1,7): expected-doctype-but-got-start-tag",
+        "(1,14): unexpected-start-tag-implies-table-voodoo",
+        "(1,17): unexpected-start-tag-implies-table-voodoo",
+        "(1,20): unexpected-start-tag-implies-table-voodoo",
+        "(1,20): closing-non-current-p-element",
+        "(1,21): foster-parenting-character",
+        "(1,22): foster-parenting-character",
+        "(1,23): foster-parenting-character",
+        "(1,24): foster-parenting-character",
+        "(1,25): foster-parenting-character",
+        "(1,26): foster-parenting-character",
+        "(1,27): foster-parenting-character",
+        "(1,28): foster-parenting-character",
+        "(1,29): foster-parenting-character",
+        "(1,30): foster-parenting-character",
+        "(1,31): foster-parenting-character",
+        "(1,32): foster-parenting-character",
+        "(1,33): foster-parenting-character",
+        "(1,34): foster-parenting-character",
+        "(1,35): foster-parenting-character",
+        "(1,36): foster-parenting-character",
+        "(1,37): foster-parenting-character",
+        "(1,38): foster-parenting-character",
+        "(1,39): foster-parenting-character",
+        "(1,40): foster-parenting-character",
+        "(1,41): foster-parenting-character",
+        "(1,42): foster-parenting-character",
+        "(1,43): foster-parenting-character",
+        "(1,44): foster-parenting-character",
+        "(1,45): foster-parenting-character",
+        "(1,45): eof-in-table"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "p": true,
+            "a": true,
+            "table": true,
+            "tbody": true,
+            "tr": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "p",
+                    "children": [
+                      {
+                        "tag": "a"
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "p",
+                    "children": [
+                      {
+                        "tag": "a",
+                        "children": [
+                          {
+                            "text": "You should see this text."
+                          }
+                        ]
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "tbody",
+                        "children": [
+                          {
+                            "tag": "tr"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><p><a></a></p><p><a>You should see this text.</a></p><table><tbody><tr></tr></tbody></table></body></html>",
+        "noQuirksBodyHtml": "<p><a></a></p><p><a>You should see this text.</a></p><table><tbody><tr></tr></tbody></table>"
+      }
+    },
+    {
+      "data": "<TABLE>\n<TR>\n<CENTER><CENTER><TD></TD></TR><TR>\n<FONT>\n<TABLE><tr></tr></TABLE>\n</P>\n<a></font><font></a>\nThis page contains an insanely badly-nested tag sequence.",
+      "errors": [
+        "(1,7): expected-doctype-but-got-start-tag",
+        "(3,8): unexpected-start-tag-implies-table-voodoo",
+        "(3,16): unexpected-start-tag-implies-table-voodoo",
+        "(4,6): unexpected-start-tag-implies-table-voodoo",
+        "(4,6): unexpected character token in table (the newline)",
+        "(5,7): unexpected-start-tag-implies-end-tag",
+        "(6,4): unexpected p end tag",
+        "(7,10): adoption-agency-1.3",
+        "(7,20): adoption-agency-1.3",
+        "(8,57): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "center": true,
+            "font": true,
+            "table": true,
+            "tbody": true,
+            "tr": true,
+            "td": true,
+            "p": true,
+            "a": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "center",
+                    "children": [
+                      {
+                        "tag": "center"
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "font",
+                    "children": [
+                      {
+                        "text": "\n"
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "text": "\n"
+                      },
+                      {
+                        "tag": "tbody",
+                        "children": [
+                          {
+                            "tag": "tr",
+                            "children": [
+                              {
+                                "text": "\n"
+                              },
+                              {
+                                "tag": "td"
+                              }
+                            ]
+                          },
+                          {
+                            "tag": "tr",
+                            "children": [
+                              {
+                                "text": "\n"
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "tbody",
+                        "children": [
+                          {
+                            "tag": "tr"
+                          }
+                        ]
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "font",
+                    "children": [
+                      {
+                        "text": "\n"
+                      },
+                      {
+                        "tag": "p"
+                      },
+                      {
+                        "text": "\n"
+                      },
+                      {
+                        "tag": "a"
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "a",
+                    "children": [
+                      {
+                        "tag": "font"
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "font",
+                    "children": [
+                      {
+                        "text": "\nThis page contains an insanely badly-nested tag sequence."
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><center><center></center></center><font>\n</font><table>\n<tbody><tr>\n<td></td></tr><tr>\n</tr></tbody></table><table><tbody><tr></tr></tbody></table><font>\n<p></p>\n<a></a></font><a><font></font></a><font>\nThis page contains an insanely badly-nested tag sequence.</font></body></html>",
+        "noQuirksBodyHtml": "<center><center></center></center><font>\n</font><table>\n<tbody><tr>\n<td></td></tr><tr>\n</tr></tbody></table><table><tbody><tr></tr></tbody></table><font>\n<p></p>\n<a></a></font><a><font></font></a><font>\nThis page contains an insanely badly-nested tag sequence.</font>"
+      }
+    },
+    {
+      "data": "<html>\n<body>\n<b><nobr><div>This text is in a div inside a nobr</nobr>More text that should not be in the nobr, i.e., the\nnobr should have closed the div inside it implicitly. </b><pre>A pre tag outside everything else.</pre>\n</body>\n</html>",
+      "errors": [
+        "(1,6): expected-doctype-but-got-start-tag",
+        "(3,56): adoption-agency-1.3",
+        "(4,58): adoption-agency-1.3",
+        "(5,7): expected-one-end-tag-but-got-another"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "b": true,
+            "nobr": true,
+            "div": true,
+            "pre": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "\n"
+                  },
+                  {
+                    "tag": "b",
+                    "children": [
+                      {
+                        "tag": "nobr"
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "div",
+                    "children": [
+                      {
+                        "tag": "b",
+                        "children": [
+                          {
+                            "tag": "nobr",
+                            "children": [
+                              {
+                                "text": "This text is in a div inside a nobr"
+                              }
+                            ]
+                          },
+                          {
+                            "text": "More text that should not be in the nobr, i.e., the\nnobr should have closed the div inside it implicitly. "
+                          }
+                        ]
+                      },
+                      {
+                        "tag": "pre",
+                        "children": [
+                          {
+                            "text": "A pre tag outside everything else."
+                          }
+                        ]
+                      },
+                      {
+                        "text": "\n\n"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>\n<b><nobr></nobr></b><div><b><nobr>This text is in a div inside a nobr</nobr>More text that should not be in the nobr, i.e., the\nnobr should have closed the div inside it implicitly. </b><pre>A pre tag outside everything else.</pre>\n\n</div></body></html>",
+        "noQuirksBodyHtml": "\n\n<b><nobr></nobr></b><div><b><nobr>This text is in a div inside a nobr</nobr>More text that should not be in the nobr, i.e., the\nnobr should have closed the div inside it implicitly. </b><pre>A pre tag outside everything else.</pre>\n\n</div>"
+      }
+    }
+  ],
+  "webkit01.dat": [
+    {
+      "data": "Test",
+      "errors": [
+        "(1,4): expected-doctype-but-got-chars"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "Test"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>Test</body></html>",
+        "noQuirksBodyHtml": "Test"
+      }
+    },
+    {
+      "data": "<div></div>",
+      "errors": [
+        "(1,5): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "div": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "div"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><div></div></body></html>",
+        "noQuirksBodyHtml": "<div></div>"
+      }
+    },
+    {
+      "data": "<div>Test</div>",
+      "errors": [
+        "(1,5): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "div": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "div",
+                    "children": [
+                      {
+                        "text": "Test"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><div>Test</div></body></html>",
+        "noQuirksBodyHtml": "<div>Test</div>"
+      }
+    },
+    {
+      "data": "<di",
+      "errors": [
+        "(1,3): eof-in-tag-name",
+        "(1,3): expected-doctype-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body></body></html>",
+        "noQuirksBodyHtml": ""
+      }
+    },
+    {
+      "data": "<div>Hello</div>\n<script>\nconsole.log(\"PASS\");\n</script>\n<div>Bye</div>",
+      "errors": [
+        "(1,5): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "div": true,
+            "script": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "div",
+                    "children": [
+                      {
+                        "text": "Hello"
+                      }
+                    ]
+                  },
+                  {
+                    "text": "\n"
+                  },
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "\nconsole.log(\"PASS\");\n",
+                        "no_escape": true
+                      }
+                    ]
+                  },
+                  {
+                    "text": "\n"
+                  },
+                  {
+                    "tag": "div",
+                    "children": [
+                      {
+                        "text": "Bye"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><div>Hello</div>\n<script>\nconsole.log(\"PASS\");\n</script>\n<div>Bye</div></body></html>",
+        "noQuirksBodyHtml": "<div>Hello</div>\n<script>\nconsole.log(\"PASS\");\n</script>\n<div>Bye</div>"
+      }
+    },
+    {
+      "data": "<div foo=\"bar\">Hello</div>",
+      "errors": [
+        "(1,15): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "div": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "div",
+                    "attrs": [
+                      {
+                        "name": "foo",
+                        "value": "bar"
+                      }
+                    ],
+                    "children": [
+                      {
+                        "text": "Hello"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><div foo=\"bar\">Hello</div></body></html>",
+        "noQuirksBodyHtml": "<div foo=\"bar\">Hello</div>"
+      }
+    },
+    {
+      "data": "<div>Hello</div>\n<script>\nconsole.log(\"FOO<span>BAR</span>BAZ\");\n</script>\n<div>Bye</div>",
+      "errors": [
+        "(1,5): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "div": true,
+            "script": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "div",
+                    "children": [
+                      {
+                        "text": "Hello"
+                      }
+                    ]
+                  },
+                  {
+                    "text": "\n"
+                  },
+                  {
+                    "tag": "script",
+                    "children": [
+                      {
+                        "text": "\nconsole.log(\"FOO<span>BAR</span>BAZ\");\n",
+                        "no_escape": true
+                      }
+                    ]
+                  },
+                  {
+                    "text": "\n"
+                  },
+                  {
+                    "tag": "div",
+                    "children": [
+                      {
+                        "text": "Bye"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><div>Hello</div>\n<script>\nconsole.log(\"FOO<span>BAR</span>BAZ\");\n</script>\n<div>Bye</div></body></html>",
+        "noQuirksBodyHtml": "<div>Hello</div>\n<script>\nconsole.log(\"FOO<span>BAR</span>BAZ\");\n</script>\n<div>Bye</div>"
+      }
+    },
+    {
+      "data": "<foo bar=\"baz\"></foo><potato quack=\"duck\"></potato>",
+      "errors": [
+        "(1,15): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "foo": true,
+            "potato": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "foo",
+                    "attrs": [
+                      {
+                        "name": "bar",
+                        "value": "baz"
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "potato",
+                    "attrs": [
+                      {
+                        "name": "quack",
+                        "value": "duck"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><foo bar=\"baz\"></foo><potato quack=\"duck\"></potato></body></html>",
+        "noQuirksBodyHtml": "<foo bar=\"baz\"></foo><potato quack=\"duck\"></potato>"
+      }
+    },
+    {
+      "data": "<foo bar=\"baz\"><potato quack=\"duck\"></potato></foo>",
+      "errors": [
+        "(1,15): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "foo": true,
+            "potato": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "foo",
+                    "attrs": [
+                      {
+                        "name": "bar",
+                        "value": "baz"
+                      }
+                    ],
+                    "children": [
+                      {
+                        "tag": "potato",
+                        "attrs": [
+                          {
+                            "name": "quack",
+                            "value": "duck"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><foo bar=\"baz\"><potato quack=\"duck\"></potato></foo></body></html>",
+        "noQuirksBodyHtml": "<foo bar=\"baz\"><potato quack=\"duck\"></potato></foo>"
+      }
+    },
+    {
+      "data": "<foo></foo bar=\"baz\"><potato></potato quack=\"duck\">",
+      "errors": [
+        "(1,5): expected-doctype-but-got-start-tag",
+        "(1,21): attributes-in-end-tag",
+        "(1,51): attributes-in-end-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "foo": true,
+            "potato": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "foo"
+                  },
+                  {
+                    "tag": "potato"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><foo></foo><potato></potato></body></html>",
+        "noQuirksBodyHtml": "<foo></foo><potato></potato>"
+      }
+    },
+    {
+      "data": "</ tttt>",
+      "errors": [
+        "(1,2): expected-closing-tag-but-got-char",
+        "(1,8): expected-doctype-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "comment": true
+        },
+        "tree": [
+          {
+            "comment": " tttt"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<!-- tttt--><html><head></head><body></body></html>",
+        "noQuirksBodyHtml": "<!-- tttt-->"
+      }
+    },
+    {
+      "data": "<div FOO ><img><img></div>",
+      "errors": [
+        "(1,10): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "div": true,
+            "img": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "div",
+                    "attrs": [
+                      {
+                        "name": "foo",
+                        "value": ""
+                      }
+                    ],
+                    "children": [
+                      {
+                        "tag": "img"
+                      },
+                      {
+                        "tag": "img"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><div foo=\"\"><img><img></div></body></html>",
+        "noQuirksBodyHtml": "<div foo=\"\"><img><img></div>"
+      }
+    },
+    {
+      "data": "<p>Test</p<p>Test2</p>",
+      "errors": [
+        "(1,3): expected-doctype-but-got-start-tag",
+        "(1,13): unexpected-end-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "p": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "p",
+                    "children": [
+                      {
+                        "text": "TestTest2"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><p>TestTest2</p></body></html>",
+        "noQuirksBodyHtml": "<p>TestTest2</p>"
+      }
+    },
+    {
+      "data": "<rdar://problem/6869687>",
+      "errors": [
+        "(1,7): unexpected-character-after-solidus-in-tag",
+        "(1,8): unexpected-character-after-solidus-in-tag",
+        "(1,16): unexpected-character-after-solidus-in-tag",
+        "(1,24): expected-doctype-but-got-start-tag",
+        "(1,24): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "rdar:": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "rdar:",
+                    "attrs": [
+                      {
+                        "name": "6869687",
+                        "value": ""
+                      },
+                      {
+                        "name": "problem",
+                        "value": ""
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><rdar: problem=\"\" 6869687=\"\"></rdar:></body></html>",
+        "noQuirksBodyHtml": "<rdar: problem=\"\" 6869687=\"\"></rdar:>"
+      }
+    },
+    {
+      "data": "<A>test< /A>",
+      "errors": [
+        "(1,3): expected-doctype-but-got-start-tag",
+        "(1,8): expected-tag-name",
+        "(1,12): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "a": true
+          },
+          "escaped": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "a",
+                    "children": [
+                      {
+                        "text": "test< /A>",
+                        "escaped": true
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><a>test&lt; /A&gt;</a></body></html>",
+        "noQuirksBodyHtml": "<a>test&lt; /A&gt;</a>"
+      }
+    },
+    {
+      "data": "&lt;",
+      "errors": [
+        "(1,4): expected-doctype-but-got-chars"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "escaped": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "<",
+                    "escaped": true
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>&lt;</body></html>",
+        "noQuirksBodyHtml": "&lt;"
+      }
+    },
+    {
+      "data": "<body foo='bar'><body foo='baz' yo='mama'>",
+      "errors": [
+        "(1,16): expected-doctype-but-got-start-tag",
+        "(1,42): unexpected-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "attrs": [
+                  {
+                    "name": "foo",
+                    "value": "bar"
+                  },
+                  {
+                    "name": "yo",
+                    "value": "mama"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body foo=\"bar\" yo=\"mama\"></body></html>",
+        "noQuirksBodyHtml": ""
+      }
+    },
+    {
+      "data": "<body></br foo=\"bar\"></body>",
+      "errors": [
+        "(1,6): expected-doctype-but-got-start-tag",
+        "(1,21): attributes-in-end-tag",
+        "(1,21): unexpected-end-tag-treated-as"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "br": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "br"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><br></body></html>",
+        "noQuirksBodyHtml": "<br>"
+      }
+    },
+    {
+      "data": "<bdy><br foo=\"bar\"></body>",
+      "errors": [
+        "(1,5): expected-doctype-but-got-start-tag",
+        "(1,26): expected-one-end-tag-but-got-another"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "bdy": true,
+            "br": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "bdy",
+                    "children": [
+                      {
+                        "tag": "br",
+                        "attrs": [
+                          {
+                            "name": "foo",
+                            "value": "bar"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><bdy><br foo=\"bar\"></bdy></body></html>",
+        "noQuirksBodyHtml": "<bdy><br foo=\"bar\"></bdy>"
+      }
+    },
+    {
+      "data": "<body></body></br foo=\"bar\">",
+      "errors": [
+        "(1,6): expected-doctype-but-got-start-tag",
+        "(1,28): attributes-in-end-tag",
+        "(1,28): unexpected-end-tag-after-body",
+        "(1,28): unexpected-end-tag-treated-as"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "br": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "br"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><br></body></html>",
+        "noQuirksBodyHtml": "<br>"
+      }
+    },
+    {
+      "data": "<bdy></body><br foo=\"bar\">",
+      "errors": [
+        "(1,5): expected-doctype-but-got-start-tag",
+        "(1,12): expected-one-end-tag-but-got-another",
+        "(1,26): unexpected-start-tag-after-body",
+        "(1,26): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "bdy": true,
+            "br": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "bdy",
+                    "children": [
+                      {
+                        "tag": "br",
+                        "attrs": [
+                          {
+                            "name": "foo",
+                            "value": "bar"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><bdy><br foo=\"bar\"></bdy></body></html>",
+        "noQuirksBodyHtml": "<bdy><br foo=\"bar\"></bdy>"
+      }
+    },
+    {
+      "data": "<html><body></body></html><!-- Hi there -->",
+      "errors": [
+        "(1,6): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "comment": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          },
+          {
+            "comment": " Hi there "
+          }
+        ],
+        "html": "<html><head></head><body></body></html><!-- Hi there -->",
+        "noQuirksBodyHtml": "<!-- Hi there -->"
+      }
+    },
+    {
+      "data": "<html><body></body></html>x<!-- Hi there -->",
+      "errors": [
+        "(1,6): expected-doctype-but-got-start-tag",
+        "(1,27): expected-eof-but-got-char"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "comment": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "x"
+                  },
+                  {
+                    "comment": " Hi there "
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>x<!-- Hi there --></body></html>",
+        "noQuirksBodyHtml": "x<!-- Hi there -->"
+      }
+    },
+    {
+      "data": "<html><body></body></html>x<!-- Hi there --></html><!-- Again -->",
+      "errors": [
+        "(1,6): expected-doctype-but-got-start-tag",
+        "(1,27): expected-eof-but-got-char"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "comment": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "x"
+                  },
+                  {
+                    "comment": " Hi there "
+                  }
+                ]
+              }
+            ]
+          },
+          {
+            "comment": " Again "
+          }
+        ],
+        "html": "<html><head></head><body>x<!-- Hi there --></body></html><!-- Again -->",
+        "noQuirksBodyHtml": "x<!-- Hi there --><!-- Again -->"
+      }
+    },
+    {
+      "data": "<html><body></body></html>x<!-- Hi there --></body></html><!-- Again -->",
+      "errors": [
+        "(1,6): expected-doctype-but-got-start-tag",
+        "(1,27): expected-eof-but-got-char"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          },
+          "comment": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "x"
+                  },
+                  {
+                    "comment": " Hi there "
+                  }
+                ]
+              }
+            ]
+          },
+          {
+            "comment": " Again "
+          }
+        ],
+        "html": "<html><head></head><body>x<!-- Hi there --></body></html><!-- Again -->",
+        "noQuirksBodyHtml": "x<!-- Hi there --><!-- Again -->"
+      }
+    },
+    {
+      "data": "<html><body><ruby><div><rp>xx</rp></div></ruby></body></html>",
+      "errors": [
+        "(1,6): expected-doctype-but-got-start-tag",
+        "(1,27): XXX-undefined-error"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "ruby": true,
+            "div": true,
+            "rp": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "ruby",
+                    "children": [
+                      {
+                        "tag": "div",
+                        "children": [
+                          {
+                            "tag": "rp",
+                            "children": [
+                              {
+                                "text": "xx"
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><ruby><div><rp>xx</rp></div></ruby></body></html>",
+        "noQuirksBodyHtml": "<ruby><div><rp>xx</rp></div></ruby>"
+      }
+    },
+    {
+      "data": "<html><body><ruby><div><rt>xx</rt></div></ruby></body></html>",
+      "errors": [
+        "(1,6): expected-doctype-but-got-start-tag",
+        "(1,27): XXX-undefined-error"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "ruby": true,
+            "div": true,
+            "rt": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "ruby",
+                    "children": [
+                      {
+                        "tag": "div",
+                        "children": [
+                          {
+                            "tag": "rt",
+                            "children": [
+                              {
+                                "text": "xx"
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><ruby><div><rt>xx</rt></div></ruby></body></html>",
+        "noQuirksBodyHtml": "<ruby><div><rt>xx</rt></div></ruby>"
+      }
+    },
+    {
+      "data": "<html><frameset><!--1--><noframes>A</noframes><!--2--></frameset><!--3--><noframes>B</noframes><!--4--></html><!--5--><noframes>C</noframes><!--6-->",
+      "errors": [
+        "(1,6): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "frameset": true,
+            "noframes": true
+          },
+          "comment": true,
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "frameset",
+                "children": [
+                  {
+                    "comment": "1"
+                  },
+                  {
+                    "tag": "noframes",
+                    "children": [
+                      {
+                        "text": "A",
+                        "no_escape": true
+                      }
+                    ]
+                  },
+                  {
+                    "comment": "2"
+                  }
+                ]
+              },
+              {
+                "comment": "3"
+              },
+              {
+                "tag": "noframes",
+                "children": [
+                  {
+                    "text": "B",
+                    "no_escape": true
+                  }
+                ]
+              },
+              {
+                "comment": "4"
+              },
+              {
+                "tag": "noframes",
+                "children": [
+                  {
+                    "text": "C",
+                    "no_escape": true
+                  }
+                ]
+              }
+            ]
+          },
+          {
+            "comment": "5"
+          },
+          {
+            "comment": "6"
+          }
+        ],
+        "html": "<html><head></head><frameset><!--1--><noframes>A</noframes><!--2--></frameset><!--3--><noframes>B</noframes><!--4--><noframes>C</noframes></html><!--5--><!--6-->",
+        "noQuirksBodyHtml": "<!--1--><noframes>A</noframes><!--2--><!--3--><noframes>B</noframes><!--4--><!--5--><noframes>C</noframes><!--6-->"
+      }
+    },
+    {
+      "data": "<select><option>A<select><option>B<select><option>C<select><option>D<select><option>E<select><option>F<select><option>G<select>",
+      "errors": [
+        "(1,8): expected-doctype-but-got-start-tag",
+        "(1,25): unexpected-select-in-select",
+        "(1,59): unexpected-select-in-select",
+        "(1,93): unexpected-select-in-select",
+        "(1,127): unexpected-select-in-select"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "select": true,
+            "option": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "select",
+                    "children": [
+                      {
+                        "tag": "option",
+                        "children": [
+                          {
+                            "text": "A"
+                          }
+                        ]
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "option",
+                    "children": [
+                      {
+                        "text": "B"
+                      },
+                      {
+                        "tag": "select",
+                        "children": [
+                          {
+                            "tag": "option",
+                            "children": [
+                              {
+                                "text": "C"
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "option",
+                    "children": [
+                      {
+                        "text": "D"
+                      },
+                      {
+                        "tag": "select",
+                        "children": [
+                          {
+                            "tag": "option",
+                            "children": [
+                              {
+                                "text": "E"
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "option",
+                    "children": [
+                      {
+                        "text": "F"
+                      },
+                      {
+                        "tag": "select",
+                        "children": [
+                          {
+                            "tag": "option",
+                            "children": [
+                              {
+                                "text": "G"
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><select><option>A</option></select><option>B<select><option>C</option></select></option><option>D<select><option>E</option></select></option><option>F<select><option>G</option></select></option></body></html>",
+        "noQuirksBodyHtml": "<select><option>A</option></select><option>B<select><option>C</option></select></option><option>D<select><option>E</option></select></option><option>F<select><option>G</option></select></option>"
+      }
+    },
+    {
+      "data": "<dd><dd><dt><dt><dd><li><li>",
+      "errors": [
+        "(1,4): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "dd": true,
+            "dt": true,
+            "li": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "dd"
+                  },
+                  {
+                    "tag": "dd"
+                  },
+                  {
+                    "tag": "dt"
+                  },
+                  {
+                    "tag": "dt"
+                  },
+                  {
+                    "tag": "dd",
+                    "children": [
+                      {
+                        "tag": "li"
+                      },
+                      {
+                        "tag": "li"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><dd></dd><dd></dd><dt></dt><dt></dt><dd><li></li><li></li></dd></body></html>",
+        "noQuirksBodyHtml": "<dd></dd><dd></dd><dt></dt><dt></dt><dd><li></li><li></li></dd>"
+      }
+    },
+    {
+      "data": "<div><b></div><div><nobr>a<nobr>",
+      "errors": [
+        "(1,5): expected-doctype-but-got-start-tag",
+        "(1,14): end-tag-too-early",
+        "(1,32): unexpected-start-tag-implies-end-tag",
+        "(1,32): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "div": true,
+            "b": true,
+            "nobr": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "div",
+                    "children": [
+                      {
+                        "tag": "b"
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "div",
+                    "children": [
+                      {
+                        "tag": "b",
+                        "children": [
+                          {
+                            "tag": "nobr",
+                            "children": [
+                              {
+                                "text": "a"
+                              }
+                            ]
+                          },
+                          {
+                            "tag": "nobr"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><div><b></b></div><div><b><nobr>a</nobr><nobr></nobr></b></div></body></html>",
+        "noQuirksBodyHtml": "<div><b></b></div><div><b><nobr>a</nobr><nobr></nobr></b></div>"
+      }
+    },
+    {
+      "data": "<head></head>\n<body></body>",
+      "errors": [
+        "(1,6): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "text": "\n"
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head>\n<body></body></html>",
+        "noQuirksBodyHtml": "\n"
+      }
+    },
+    {
+      "data": "<head></head> <style></style>ddd",
+      "errors": [
+        "(1,6): expected-doctype-but-got-start-tag",
+        "(1,21): unexpected-start-tag-out-of-my-head"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "style": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head",
+                "children": [
+                  {
+                    "tag": "style"
+                  }
+                ]
+              },
+              {
+                "text": " "
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "ddd"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head><style></style></head> <body>ddd</body></html>",
+        "noQuirksBodyHtml": " <style></style>ddd"
+      }
+    },
+    {
+      "data": "<kbd><table></kbd><col><select><tr>",
+      "errors": [
+        "(1,5): expected-doctype-but-got-start-tag",
+        "(1,18): unexpected-end-tag-implies-table-voodoo",
+        "(1,18): unexpected-end-tag",
+        "(1,31): unexpected-start-tag-implies-table-voodoo",
+        "(1,35): unexpected-table-element-start-tag-in-select-in-table",
+        "(1,35): eof-in-table"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "kbd": true,
+            "select": true,
+            "table": true,
+            "colgroup": true,
+            "col": true,
+            "tbody": true,
+            "tr": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "kbd",
+                    "children": [
+                      {
+                        "tag": "select"
+                      },
+                      {
+                        "tag": "table",
+                        "children": [
+                          {
+                            "tag": "colgroup",
+                            "children": [
+                              {
+                                "tag": "col"
+                              }
+                            ]
+                          },
+                          {
+                            "tag": "tbody",
+                            "children": [
+                              {
+                                "tag": "tr"
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><kbd><select></select><table><colgroup><col></colgroup><tbody><tr></tr></tbody></table></kbd></body></html>",
+        "noQuirksBodyHtml": "<kbd><select></select><table><colgroup><col></colgroup><tbody><tr></tr></tbody></table></kbd>"
+      }
+    },
+    {
+      "data": "<kbd><table></kbd><col><select><tr></table><div>",
+      "errors": [
+        "(1,5): expected-doctype-but-got-start-tag",
+        "(1,18): unexpected-end-tag-implies-table-voodoo",
+        "(1,18): unexpected-end-tag",
+        "(1,31): unexpected-start-tag-implies-table-voodoo",
+        "(1,35): unexpected-table-element-start-tag-in-select-in-table",
+        "(1,48): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "kbd": true,
+            "select": true,
+            "table": true,
+            "colgroup": true,
+            "col": true,
+            "tbody": true,
+            "tr": true,
+            "div": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "kbd",
+                    "children": [
+                      {
+                        "tag": "select"
+                      },
+                      {
+                        "tag": "table",
+                        "children": [
+                          {
+                            "tag": "colgroup",
+                            "children": [
+                              {
+                                "tag": "col"
+                              }
+                            ]
+                          },
+                          {
+                            "tag": "tbody",
+                            "children": [
+                              {
+                                "tag": "tr"
+                              }
+                            ]
+                          }
+                        ]
+                      },
+                      {
+                        "tag": "div"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><kbd><select></select><table><colgroup><col></colgroup><tbody><tr></tr></tbody></table><div></div></kbd></body></html>",
+        "noQuirksBodyHtml": "<kbd><select></select><table><colgroup><col></colgroup><tbody><tr></tr></tbody></table><div></div></kbd>"
+      }
+    },
+    {
+      "data": "<a><li><style></style><title></title></a>",
+      "errors": [
+        "(1,3): expected-doctype-but-got-start-tag",
+        "(1,41): adoption-agency-1.3"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "a": true,
+            "li": true,
+            "style": true,
+            "title": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "a"
+                  },
+                  {
+                    "tag": "li",
+                    "children": [
+                      {
+                        "tag": "a",
+                        "children": [
+                          {
+                            "tag": "style"
+                          },
+                          {
+                            "tag": "title"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><a></a><li><a><style></style><title></title></a></li></body></html>",
+        "noQuirksBodyHtml": "<a></a><li><a><style></style><title></title></a></li>"
+      }
+    },
+    {
+      "data": "<font></p><p><meta><title></title></font>",
+      "errors": [
+        "(1,6): expected-doctype-but-got-start-tag",
+        "(1,10): unexpected-end-tag",
+        "(1,41): adoption-agency-1.3"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "font": true,
+            "p": true,
+            "meta": true,
+            "title": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "font",
+                    "children": [
+                      {
+                        "tag": "p"
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "p",
+                    "children": [
+                      {
+                        "tag": "font",
+                        "children": [
+                          {
+                            "tag": "meta"
+                          },
+                          {
+                            "tag": "title"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><font><p></p></font><p><font><meta><title></title></font></p></body></html>",
+        "noQuirksBodyHtml": "<font><p></p></font><p><font><meta><title></title></font></p>"
+      }
+    },
+    {
+      "data": "<a><center><title></title><a>",
+      "errors": [
+        "(1,3): expected-doctype-but-got-start-tag",
+        "(1,29): unexpected-start-tag-implies-end-tag",
+        "(1,29): adoption-agency-1.3",
+        "(1,29): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "a": true,
+            "center": true,
+            "title": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "a"
+                  },
+                  {
+                    "tag": "center",
+                    "children": [
+                      {
+                        "tag": "a",
+                        "children": [
+                          {
+                            "tag": "title"
+                          }
+                        ]
+                      },
+                      {
+                        "tag": "a"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><a></a><center><a><title></title></a><a></a></center></body></html>",
+        "noQuirksBodyHtml": "<a></a><center><a><title></title></a><a></a></center>"
+      }
+    },
+    {
+      "data": "<svg><title><div>",
+      "errors": [
+        "(1,5): expected-doctype-but-got-start-tag",
+        "(1,17): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "svg svg": true,
+            "svg title": true,
+            "div": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "svg",
+                    "ns": "http://www.w3.org/2000/svg",
+                    "children": [
+                      {
+                        "tag": "title",
+                        "ns": "http://www.w3.org/2000/svg",
+                        "children": [
+                          {
+                            "tag": "div"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><svg><title><div></div></title></svg></body></html>",
+        "noQuirksBodyHtml": "<svg><title><div></div></title></svg>"
+      }
+    },
+    {
+      "data": "<svg><title><rect><div>",
+      "errors": [
+        "(1,5): expected-doctype-but-got-start-tag",
+        "(1,23): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "svg svg": true,
+            "svg title": true,
+            "rect": true,
+            "div": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "svg",
+                    "ns": "http://www.w3.org/2000/svg",
+                    "children": [
+                      {
+                        "tag": "title",
+                        "ns": "http://www.w3.org/2000/svg",
+                        "children": [
+                          {
+                            "tag": "rect",
+                            "children": [
+                              {
+                                "tag": "div"
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><svg><title><rect><div></div></rect></title></svg></body></html>",
+        "noQuirksBodyHtml": "<svg><title><rect><div></div></rect></title></svg>"
+      }
+    },
+    {
+      "data": "<svg><title><svg><div>",
+      "errors": [
+        "(1,5): expected-doctype-but-got-start-tag",
+        "(1,22): unexpected-html-element-in-foreign-content",
+        "(1,22): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "svg svg": true,
+            "svg title": true,
+            "div": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "svg",
+                    "ns": "http://www.w3.org/2000/svg",
+                    "children": [
+                      {
+                        "tag": "title",
+                        "ns": "http://www.w3.org/2000/svg",
+                        "children": [
+                          {
+                            "tag": "svg",
+                            "ns": "http://www.w3.org/2000/svg"
+                          },
+                          {
+                            "tag": "div"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><svg><title><svg></svg><div></div></title></svg></body></html>",
+        "noQuirksBodyHtml": "<svg><title><svg><div></div></svg></title></svg>"
+      }
+    },
+    {
+      "data": "<img <=\"\" FAIL>",
+      "errors": [
+        "(1,6): invalid-character-in-attribute-name",
+        "(1,15): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "img": true
+          },
+          "attrWithFunnyChar": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "img",
+                    "attrs": [
+                      {
+                        "name": "<",
+                        "value": ""
+                      },
+                      {
+                        "name": "fail",
+                        "value": ""
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><img <=\"\" fail=\"\"></body></html>",
+        "noQuirksBodyHtml": "<img <=\"\" fail=\"\">"
+      }
+    },
+    {
+      "data": "<ul><li><div id='foo'/>A</li><li>B<div>C</div></li></ul>",
+      "errors": [
+        "(1,4): expected-doctype-but-got-start-tag",
+        "(1,23): non-void-element-with-trailing-solidus",
+        "(1,29): end-tag-too-early"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "ul": true,
+            "li": true,
+            "div": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "ul",
+                    "children": [
+                      {
+                        "tag": "li",
+                        "children": [
+                          {
+                            "tag": "div",
+                            "attrs": [
+                              {
+                                "name": "id",
+                                "value": "foo"
+                              }
+                            ],
+                            "children": [
+                              {
+                                "text": "A"
+                              }
+                            ]
+                          }
+                        ]
+                      },
+                      {
+                        "tag": "li",
+                        "children": [
+                          {
+                            "text": "B"
+                          },
+                          {
+                            "tag": "div",
+                            "children": [
+                              {
+                                "text": "C"
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><ul><li><div id=\"foo\">A</div></li><li>B<div>C</div></li></ul></body></html>",
+        "noQuirksBodyHtml": "<ul><li><div id=\"foo\">A</div></li><li>B<div>C</div></li></ul>"
+      }
+    },
+    {
+      "data": "<svg><em><desc></em>",
+      "errors": [
+        "(1,5): expected-doctype-but-got-start-tag",
+        "(1,9): unexpected-html-element-in-foreign-content",
+        "(1,20): adoption-agency-1.3"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "svg svg": true,
+            "em": true,
+            "desc": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "svg",
+                    "ns": "http://www.w3.org/2000/svg"
+                  },
+                  {
+                    "tag": "em",
+                    "children": [
+                      {
+                        "tag": "desc"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><svg></svg><em><desc></desc></em></body></html>",
+        "noQuirksBodyHtml": "<svg><em><desc></desc></em></svg>"
+      }
+    },
+    {
+      "data": "<table><tr><td><svg><desc><td></desc><circle>",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true,
+            "tbody": true,
+            "tr": true,
+            "td": true,
+            "svg svg": true,
+            "svg desc": true,
+            "circle": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "tbody",
+                        "children": [
+                          {
+                            "tag": "tr",
+                            "children": [
+                              {
+                                "tag": "td",
+                                "children": [
+                                  {
+                                    "tag": "svg",
+                                    "ns": "http://www.w3.org/2000/svg",
+                                    "children": [
+                                      {
+                                        "tag": "desc",
+                                        "ns": "http://www.w3.org/2000/svg"
+                                      }
+                                    ]
+                                  }
+                                ]
+                              },
+                              {
+                                "tag": "td",
+                                "children": [
+                                  {
+                                    "tag": "circle"
+                                  }
+                                ]
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><table><tbody><tr><td><svg><desc></desc></svg></td><td><circle></circle></td></tr></tbody></table></body></html>",
+        "noQuirksBodyHtml": "<table><tbody><tr><td><svg><desc></desc></svg></td><td><circle></circle></td></tr></tbody></table>"
+      }
+    },
+    {
+      "data": "<svg><tfoot></mi><td>",
+      "errors": [
+        "(1,5): expected-doctype-but-got-start-tag",
+        "(1,17): unexpected-end-tag",
+        "(1,17): unexpected-end-tag",
+        "(1,21): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "svg svg": true,
+            "svg tfoot": true,
+            "svg td": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "svg",
+                    "ns": "http://www.w3.org/2000/svg",
+                    "children": [
+                      {
+                        "tag": "tfoot",
+                        "ns": "http://www.w3.org/2000/svg",
+                        "children": [
+                          {
+                            "tag": "td",
+                            "ns": "http://www.w3.org/2000/svg"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><svg><tfoot><td></td></tfoot></svg></body></html>",
+        "noQuirksBodyHtml": "<svg><tfoot><td></td></tfoot></svg>"
+      }
+    },
+    {
+      "data": "<math><mrow><mrow><mn>1</mn></mrow><mi>a</mi></mrow></math>",
+      "errors": [
+        "(1,6): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "math math": true,
+            "math mrow": true,
+            "math mn": true,
+            "math mi": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "math",
+                    "ns": "http://www.w3.org/1998/Math/MathML",
+                    "children": [
+                      {
+                        "tag": "mrow",
+                        "ns": "http://www.w3.org/1998/Math/MathML",
+                        "children": [
+                          {
+                            "tag": "mrow",
+                            "ns": "http://www.w3.org/1998/Math/MathML",
+                            "children": [
+                              {
+                                "tag": "mn",
+                                "ns": "http://www.w3.org/1998/Math/MathML",
+                                "children": [
+                                  {
+                                    "text": "1"
+                                  }
+                                ]
+                              }
+                            ]
+                          },
+                          {
+                            "tag": "mi",
+                            "ns": "http://www.w3.org/1998/Math/MathML",
+                            "children": [
+                              {
+                                "text": "a"
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><math><mrow><mrow><mn>1</mn></mrow><mi>a</mi></mrow></math></body></html>",
+        "noQuirksBodyHtml": "<math><mrow><mrow><mn>1</mn></mrow><mi>a</mi></mrow></math>"
+      }
+    },
+    {
+      "data": "<!doctype html><input type=\"hidden\"><frameset>",
+      "errors": [
+        "(1,46): unexpected-start-tag",
+        "(1,46): eof-in-frameset"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "frameset": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "frameset"
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><frameset></frameset></html>",
+        "noQuirksBodyHtml": "<input type=\"hidden\">"
+      }
+    },
+    {
+      "data": "<!doctype html><input type=\"button\"><frameset>",
+      "errors": [
+        "(1,46): unexpected-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "input": true
+          },
+          "doctype": true
+        },
+        "tree": [
+          {
+            "doctype": "html"
+          },
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "input",
+                    "attrs": [
+                      {
+                        "name": "type",
+                        "value": "button"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<!DOCTYPE html><html><head></head><body><input type=\"button\"></body></html>",
+        "noQuirksBodyHtml": "<input type=\"button\">"
+      }
+    }
+  ],
+  "webkit02.dat": [
+    {
+      "data": "<foo bar=qux/>",
+      "errors": [
+        "(1,14): expected-doctype-but-got-start-tag",
+        "(1,14): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "foo": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "foo",
+                    "attrs": [
+                      {
+                        "name": "bar",
+                        "value": "qux/"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><foo bar=\"qux/\"></foo></body></html>",
+        "noQuirksBodyHtml": "<foo bar=\"qux/\"></foo>"
+      }
+    },
+    {
+      "data": "<p id=\"status\"><noscript><strong>A</strong></noscript><span>B</span></p>",
+      "errors": [
+        "(1,15): expected-doctype-but-got-start-tag"
+      ],
+      "script": "on",
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "p": true,
+            "noscript": true,
+            "span": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "p",
+                    "attrs": [
+                      {
+                        "name": "id",
+                        "value": "status"
+                      }
+                    ],
+                    "children": [
+                      {
+                        "tag": "noscript",
+                        "children": [
+                          {
+                            "text": "<strong>A</strong>",
+                            "no_escape": true
+                          }
+                        ]
+                      },
+                      {
+                        "tag": "span",
+                        "children": [
+                          {
+                            "text": "B"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><p id=\"status\"><noscript><strong>A</strong></noscript><span>B</span></p></body></html>",
+        "noQuirksBodyHtml": "<p id=\"status\"><noscript><strong>A</strong></noscript><span>B</span></p>"
+      }
+    },
+    {
+      "data": "<p id=\"status\"><noscript><strong>A</strong></noscript><span>B</span></p>",
+      "errors": [
+        "(1,15): expected-doctype-but-got-start-tag"
+      ],
+      "script": "off",
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "p": true,
+            "noscript": true,
+            "strong": true,
+            "span": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "p",
+                    "attrs": [
+                      {
+                        "name": "id",
+                        "value": "status"
+                      }
+                    ],
+                    "children": [
+                      {
+                        "tag": "noscript",
+                        "children": [
+                          {
+                            "tag": "strong",
+                            "children": [
+                              {
+                                "text": "A"
+                              }
+                            ]
+                          }
+                        ]
+                      },
+                      {
+                        "tag": "span",
+                        "children": [
+                          {
+                            "text": "B"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><p id=\"status\"><noscript><strong>A</strong></noscript><span>B</span></p></body></html>",
+        "noQuirksBodyHtml": "<p id=\"status\"><noscript><strong>A</strong></noscript><span>B</span></p>"
+      }
+    },
+    {
+      "data": "<div><sarcasm><div></div></sarcasm></div>",
+      "errors": [
+        "(1,5): expected-doctype-but-got-start-tag"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "div": true,
+            "sarcasm": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "div",
+                    "children": [
+                      {
+                        "tag": "sarcasm",
+                        "children": [
+                          {
+                            "tag": "div"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><div><sarcasm><div></div></sarcasm></div></body></html>",
+        "noQuirksBodyHtml": "<div><sarcasm><div></div></sarcasm></div>"
+      }
+    },
+    {
+      "data": "<html><body><img src=\"\" border=\"0\" alt=\"><div>A</div></body></html>",
+      "errors": [
+        "(1,6): expected-doctype-but-got-start-tag",
+        "(1,67): eof-in-attribute-value-double-quote"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body"
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body></body></html>",
+        "noQuirksBodyHtml": ""
+      }
+    },
+    {
+      "data": "<table><td></tbody>A",
+      "errors": [
+        "(1,7): expected-doctype-but-got-start-tag",
+        "(1,11): unexpected-cell-in-table-body",
+        "(1,20): foster-parenting-character",
+        "(1,20): eof-in-table"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true,
+            "tbody": true,
+            "tr": true,
+            "td": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "text": "A"
+                  },
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "tbody",
+                        "children": [
+                          {
+                            "tag": "tr",
+                            "children": [
+                              {
+                                "tag": "td"
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body>A<table><tbody><tr><td></td></tr></tbody></table></body></html>",
+        "noQuirksBodyHtml": "A<table><tbody><tr><td></td></tr></tbody></table>"
+      }
+    },
+    {
+      "data": "<table><td></thead>A",
+      "errors": [
+        "(1,7): expected-doctype-but-got-start-tag",
+        "(1,11): unexpected-cell-in-table-body",
+        "(1,19): XXX-undefined-error",
+        "(1,20): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true,
+            "tbody": true,
+            "tr": true,
+            "td": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "tbody",
+                        "children": [
+                          {
+                            "tag": "tr",
+                            "children": [
+                              {
+                                "tag": "td",
+                                "children": [
+                                  {
+                                    "text": "A"
+                                  }
+                                ]
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><table><tbody><tr><td>A</td></tr></tbody></table></body></html>",
+        "noQuirksBodyHtml": "<table><tbody><tr><td>A</td></tr></tbody></table>"
+      }
+    },
+    {
+      "data": "<table><td></tfoot>A",
+      "errors": [
+        "(1,7): expected-doctype-but-got-start-tag",
+        "(1,11): unexpected-cell-in-table-body",
+        "(1,19): XXX-undefined-error",
+        "(1,20): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true,
+            "tbody": true,
+            "tr": true,
+            "td": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "tbody",
+                        "children": [
+                          {
+                            "tag": "tr",
+                            "children": [
+                              {
+                                "tag": "td",
+                                "children": [
+                                  {
+                                    "text": "A"
+                                  }
+                                ]
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><table><tbody><tr><td>A</td></tr></tbody></table></body></html>",
+        "noQuirksBodyHtml": "<table><tbody><tr><td>A</td></tr></tbody></table>"
+      }
+    },
+    {
+      "data": "<table><thead><td></tbody>A",
+      "errors": [
+        "(1,7): expected-doctype-but-got-start-tag",
+        "(1,18): unexpected-cell-in-table-body",
+        "(1,26): XXX-undefined-error",
+        "(1,27): expected-closing-tag-but-got-eof"
+      ],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "table": true,
+            "thead": true,
+            "tr": true,
+            "td": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "table",
+                    "children": [
+                      {
+                        "tag": "thead",
+                        "children": [
+                          {
+                            "tag": "tr",
+                            "children": [
+                              {
+                                "tag": "td",
+                                "children": [
+                                  {
+                                    "text": "A"
+                                  }
+                                ]
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><table><thead><tr><td>A</td></tr></thead></table></body></html>",
+        "noQuirksBodyHtml": "<table><thead><tr><td>A</td></tr></thead></table>"
+      }
+    },
+    {
+      "data": "<legend>test</legend>",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "legend": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "legend",
+                    "children": [
+                      {
+                        "text": "test"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><legend>test</legend></body></html>",
+        "noQuirksBodyHtml": "<legend>test</legend>"
+      }
+    },
+    {
+      "data": "<table><input>",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "input": true,
+            "table": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "input"
+                  },
+                  {
+                    "tag": "table"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><input><table></table></body></html>",
+        "noQuirksBodyHtml": "<input><table></table>"
+      }
+    },
+    {
+      "data": "<b><em><foo><foo><aside></b>",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "b": true,
+            "em": true,
+            "foo": true,
+            "aside": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "b",
+                    "children": [
+                      {
+                        "tag": "em",
+                        "children": [
+                          {
+                            "tag": "foo",
+                            "children": [
+                              {
+                                "tag": "foo"
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "em",
+                    "children": [
+                      {
+                        "tag": "aside",
+                        "children": [
+                          {
+                            "tag": "b"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><b><em><foo><foo></foo></foo></em></b><em><aside><b></b></aside></em></body></html>",
+        "noQuirksBodyHtml": "<b><em><foo><foo></foo></foo></em></b><em><aside><b></b></aside></em>"
+      }
+    },
+    {
+      "data": "<b><em><foo><foo><aside></b></em>",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "b": true,
+            "em": true,
+            "foo": true,
+            "aside": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "b",
+                    "children": [
+                      {
+                        "tag": "em",
+                        "children": [
+                          {
+                            "tag": "foo",
+                            "children": [
+                              {
+                                "tag": "foo"
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "em"
+                  },
+                  {
+                    "tag": "aside",
+                    "children": [
+                      {
+                        "tag": "em",
+                        "children": [
+                          {
+                            "tag": "b"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><b><em><foo><foo></foo></foo></em></b><em></em><aside><em><b></b></em></aside></body></html>",
+        "noQuirksBodyHtml": "<b><em><foo><foo></foo></foo></em></b><em></em><aside><em><b></b></em></aside>"
+      }
+    },
+    {
+      "data": "<b><em><foo><foo><foo><aside></b>",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "b": true,
+            "em": true,
+            "foo": true,
+            "aside": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "b",
+                    "children": [
+                      {
+                        "tag": "em",
+                        "children": [
+                          {
+                            "tag": "foo",
+                            "children": [
+                              {
+                                "tag": "foo",
+                                "children": [
+                                  {
+                                    "tag": "foo"
+                                  }
+                                ]
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "aside",
+                    "children": [
+                      {
+                        "tag": "b"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><b><em><foo><foo><foo></foo></foo></foo></em></b><aside><b></b></aside></body></html>",
+        "noQuirksBodyHtml": "<b><em><foo><foo><foo></foo></foo></foo></em></b><aside><b></b></aside>"
+      }
+    },
+    {
+      "data": "<b><em><foo><foo><foo><aside></b></em>",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "b": true,
+            "em": true,
+            "foo": true,
+            "aside": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "b",
+                    "children": [
+                      {
+                        "tag": "em",
+                        "children": [
+                          {
+                            "tag": "foo",
+                            "children": [
+                              {
+                                "tag": "foo",
+                                "children": [
+                                  {
+                                    "tag": "foo"
+                                  }
+                                ]
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  },
+                  {
+                    "tag": "aside",
+                    "children": [
+                      {
+                        "tag": "b"
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><b><em><foo><foo><foo></foo></foo></foo></em></b><aside><b></b></aside></body></html>",
+        "noQuirksBodyHtml": "<b><em><foo><foo><foo></foo></foo></foo></em></b><aside><b></b></aside>"
+      }
+    },
+    {
+      "data": "<b><em><foo><foo><foo><foo><foo><foo><foo><foo><foo><foo><aside></b></em>",
+      "errors": [],
+      "fragment": {
+        "name": "div"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "b": true,
+            "em": true,
+            "foo": true,
+            "aside": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "b",
+            "children": [
+              {
+                "tag": "em",
+                "children": [
+                  {
+                    "tag": "foo",
+                    "children": [
+                      {
+                        "tag": "foo",
+                        "children": [
+                          {
+                            "tag": "foo",
+                            "children": [
+                              {
+                                "tag": "foo",
+                                "children": [
+                                  {
+                                    "tag": "foo",
+                                    "children": [
+                                      {
+                                        "tag": "foo",
+                                        "children": [
+                                          {
+                                            "tag": "foo",
+                                            "children": [
+                                              {
+                                                "tag": "foo",
+                                                "children": [
+                                                  {
+                                                    "tag": "foo",
+                                                    "children": [
+                                                      {
+                                                        "tag": "foo"
+                                                      }
+                                                    ]
+                                                  }
+                                                ]
+                                              }
+                                            ]
+                                          }
+                                        ]
+                                      }
+                                    ]
+                                  }
+                                ]
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          },
+          {
+            "tag": "aside",
+            "children": [
+              {
+                "tag": "b"
+              }
+            ]
+          }
+        ],
+        "html": "<b><em><foo><foo><foo><foo><foo><foo><foo><foo><foo><foo></foo></foo></foo></foo></foo></foo></foo></foo></foo></foo></em></b><aside><b></b></aside>",
+        "noQuirksBodyHtml": "<b><em><foo><foo><foo><foo><foo><foo><foo><foo><foo><foo></foo></foo></foo></foo></foo></foo></foo></foo></foo></foo></em></b><aside><b></b></aside>"
+      }
+    },
+    {
+      "data": "<b><em><foo><foob><foob><foob><foob><fooc><fooc><fooc><fooc><food><aside></b></em>",
+      "errors": [],
+      "fragment": {
+        "name": "div"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "b": true,
+            "em": true,
+            "foo": true,
+            "foob": true,
+            "fooc": true,
+            "food": true,
+            "aside": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "b",
+            "children": [
+              {
+                "tag": "em",
+                "children": [
+                  {
+                    "tag": "foo",
+                    "children": [
+                      {
+                        "tag": "foob",
+                        "children": [
+                          {
+                            "tag": "foob",
+                            "children": [
+                              {
+                                "tag": "foob",
+                                "children": [
+                                  {
+                                    "tag": "foob",
+                                    "children": [
+                                      {
+                                        "tag": "fooc",
+                                        "children": [
+                                          {
+                                            "tag": "fooc",
+                                            "children": [
+                                              {
+                                                "tag": "fooc",
+                                                "children": [
+                                                  {
+                                                    "tag": "fooc",
+                                                    "children": [
+                                                      {
+                                                        "tag": "food"
+                                                      }
+                                                    ]
+                                                  }
+                                                ]
+                                              }
+                                            ]
+                                          }
+                                        ]
+                                      }
+                                    ]
+                                  }
+                                ]
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          },
+          {
+            "tag": "aside",
+            "children": [
+              {
+                "tag": "b"
+              }
+            ]
+          }
+        ],
+        "html": "<b><em><foo><foob><foob><foob><foob><fooc><fooc><fooc><fooc><food></food></fooc></fooc></fooc></fooc></foob></foob></foob></foob></foo></em></b><aside><b></b></aside>",
+        "noQuirksBodyHtml": "<b><em><foo><foob><foob><foob><foob><fooc><fooc><fooc><fooc><food></food></fooc></fooc></fooc></fooc></foob></foob></foob></foob></foo></em></b><aside><b></b></aside>"
+      }
+    },
+    {
+      "data": "<option><XH<optgroup></optgroup>",
+      "errors": [],
+      "fragment": {
+        "name": "select"
+      },
+      "document": {
+        "props": {
+          "tags": {
+            "option": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "option"
+          }
+        ],
+        "html": "<option></option>",
+        "noQuirksBodyHtml": "<option><xh<optgroup></xh<optgroup></option>"
+      }
+    },
+    {
+      "data": "<svg><foreignObject><div>foo</div><plaintext></foreignObject></svg><div>bar</div>",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "svg svg": true,
+            "svg foreignObject": true,
+            "div": true,
+            "plaintext": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "svg",
+                    "ns": "http://www.w3.org/2000/svg",
+                    "children": [
+                      {
+                        "tag": "foreignObject",
+                        "ns": "http://www.w3.org/2000/svg",
+                        "children": [
+                          {
+                            "tag": "div",
+                            "children": [
+                              {
+                                "text": "foo"
+                              }
+                            ]
+                          },
+                          {
+                            "tag": "plaintext",
+                            "children": [
+                              {
+                                "text": "</foreignObject></svg><div>bar</div>",
+                                "no_escape": true
+                              }
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><svg><foreignObject><div>foo</div><plaintext></foreignObject></svg><div>bar</div></plaintext></foreignObject></svg></body></html>",
+        "noQuirksBodyHtml": "<svg><foreignObject><div>foo</div><plaintext></foreignObject></svg><div>bar</div></plaintext></foreignObject></svg>"
+      }
+    },
+    {
+      "data": "<svg><foreignObject></foreignObject><title></svg>foo",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "svg svg": true,
+            "svg foreignObject": true,
+            "svg title": true
+          }
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "svg",
+                    "ns": "http://www.w3.org/2000/svg",
+                    "children": [
+                      {
+                        "tag": "foreignObject",
+                        "ns": "http://www.w3.org/2000/svg"
+                      },
+                      {
+                        "tag": "title",
+                        "ns": "http://www.w3.org/2000/svg"
+                      }
+                    ]
+                  },
+                  {
+                    "text": "foo"
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><svg><foreignObject></foreignObject><title></title></svg>foo</body></html>",
+        "noQuirksBodyHtml": "<svg><foreignObject></foreignObject><title></title></svg>foo"
+      }
+    },
+    {
+      "data": "</foreignObject><plaintext><div>foo</div>",
+      "errors": [],
+      "document": {
+        "props": {
+          "tags": {
+            "html": true,
+            "head": true,
+            "body": true,
+            "plaintext": true
+          },
+          "no_escape": true
+        },
+        "tree": [
+          {
+            "tag": "html",
+            "children": [
+              {
+                "tag": "head"
+              },
+              {
+                "tag": "body",
+                "children": [
+                  {
+                    "tag": "plaintext",
+                    "children": [
+                      {
+                        "text": "<div>foo</div>",
+                        "no_escape": true
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ],
+        "html": "<html><head></head><body><plaintext><div>foo</div></plaintext></body></html>",
+        "noQuirksBodyHtml": "<plaintext><div>foo</div></plaintext>"
+      }
+    }
+  ]
+}
\ No newline at end of file
diff --git a/tests/phpunit/unit/includes/title/ForeignTitleTest.php b/tests/phpunit/unit/includes/title/ForeignTitleTest.php
new file mode 100644 (file)
index 0000000..ec093cf
--- /dev/null
@@ -0,0 +1,103 @@
+<?php
+/**
+ * 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; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * 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.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @author This, that and the other
+ */
+
+/**
+ * @covers ForeignTitle
+ *
+ * @group Title
+ */
+class ForeignTitleTest extends \MediaWikiUnitTestCase {
+
+       public function basicProvider() {
+               return [
+                       [
+                               new ForeignTitle( 20, 'Contributor', 'JohnDoe' ),
+                               20, 'Contributor', 'JohnDoe'
+                       ],
+                       [
+                               new ForeignTitle( '1', 'Discussion', 'Capital' ),
+                               1, 'Discussion', 'Capital'
+                       ],
+                       [
+                               new ForeignTitle( 0, '', 'MainNamespace' ),
+                               0, '', 'MainNamespace'
+                       ],
+                       [
+                               new ForeignTitle( 4, 'Some ns', 'Article title with spaces' ),
+                               4, 'Some_ns', 'Article_title_with_spaces'
+                       ],
+               ];
+       }
+
+       /**
+        * @dataProvider basicProvider
+        */
+       public function testBasic( ForeignTitle $title, $expectedId, $expectedName,
+               $expectedText
+       ) {
+               $this->assertEquals( true, $title->isNamespaceIdKnown() );
+               $this->assertEquals( $expectedId, $title->getNamespaceId() );
+               $this->assertEquals( $expectedName, $title->getNamespaceName() );
+               $this->assertEquals( $expectedText, $title->getText() );
+       }
+
+       public function testUnknownNamespaceCheck() {
+               $title = new ForeignTitle( null, 'this', 'that' );
+
+               $this->assertEquals( false, $title->isNamespaceIdKnown() );
+               $this->assertEquals( 'this', $title->getNamespaceName() );
+               $this->assertEquals( 'that', $title->getText() );
+       }
+
+       public function testUnknownNamespaceError() {
+               $this->setExpectedException( MWException::class );
+               $title = new ForeignTitle( null, 'this', 'that' );
+               $title->getNamespaceId();
+       }
+
+       public function fullTextProvider() {
+               return [
+                       [
+                               new ForeignTitle( 20, 'Contributor', 'JohnDoe' ),
+                               'Contributor:JohnDoe'
+                       ],
+                       [
+                               new ForeignTitle( '1', 'Discussion', 'Capital' ),
+                               'Discussion:Capital'
+                       ],
+                       [
+                               new ForeignTitle( 0, '', 'MainNamespace' ),
+                               'MainNamespace'
+                       ],
+                       [
+                               new ForeignTitle( 4, 'Some ns', 'Article title with spaces' ),
+                               'Some_ns:Article_title_with_spaces'
+                       ],
+               ];
+       }
+
+       /**
+        * @dataProvider fullTextProvider
+        */
+       public function testFullText( ForeignTitle $title, $fullText ) {
+               $this->assertEquals( $fullText, $title->getFullText() );
+       }
+}
diff --git a/tests/phpunit/unit/includes/title/NaiveForeignTitleFactoryTest.php b/tests/phpunit/unit/includes/title/NaiveForeignTitleFactoryTest.php
new file mode 100644 (file)
index 0000000..de6650a
--- /dev/null
@@ -0,0 +1,91 @@
+<?php
+/**
+ * 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; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * 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.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @author This, that and the other
+ */
+
+/**
+ * @covers NaiveForeignTitleFactory
+ *
+ * @group Title
+ */
+class NaiveForeignTitleFactoryTest extends \MediaWikiUnitTestCase {
+
+       public function basicProvider() {
+               return [
+                       [
+                               'MainNamespaceArticle', 0,
+                               new ForeignTitle( 0, '', 'MainNamespaceArticle' ),
+                       ],
+                       [
+                               'MainNamespaceArticle', null,
+                               new ForeignTitle( null, '', 'MainNamespaceArticle' ),
+                       ],
+                       [
+                               'Talk:Nice_talk', 1,
+                               new ForeignTitle( 1, 'Talk', 'Nice_talk' ),
+                       ],
+                       [
+                               'Bogus:Nice_talk', 0,
+                               new ForeignTitle( 0, '', 'Bogus:Nice_talk' ),
+                       ],
+                       [
+                               'Bogus:Nice_talk', 9000, // non-existent local namespace ID
+                               new ForeignTitle( 9000, 'Bogus', 'Nice_talk' ),
+                       ],
+                       [
+                               'Bogus:Nice_talk', 4, // existing local namespace ID
+                               new ForeignTitle( 4, 'Bogus', 'Nice_talk' ),
+                       ],
+                       [
+                               'Talk:Extra:Nice_talk', 1,
+                               new ForeignTitle( 1, 'Talk', 'Extra:Nice_talk' ),
+                       ],
+                       [
+                               'Talk:Extra:Nice_talk', null,
+                               new ForeignTitle( null, 'Talk', 'Extra:Nice_talk' ),
+                       ],
+               ];
+       }
+
+       /**
+        * @dataProvider basicProvider
+        */
+       public function testBasic( $title, $ns, ForeignTitle $foreignTitle ) {
+               $factory = new NaiveForeignTitleFactory();
+               $testTitle = $factory->createForeignTitle( $title, $ns );
+
+               $this->assertEquals( $testTitle->isNamespaceIdKnown(),
+                       $foreignTitle->isNamespaceIdKnown() );
+
+               if (
+                       $testTitle->isNamespaceIdKnown() &&
+                       $foreignTitle->isNamespaceIdKnown()
+               ) {
+                       $this->assertEquals( $testTitle->getNamespaceId(),
+                               $foreignTitle->getNamespaceId() );
+               }
+
+               $this->assertEquals( $testTitle->getNamespaceName(),
+                       $foreignTitle->getNamespaceName() );
+               $this->assertEquals( $testTitle->getText(), $foreignTitle->getText() );
+
+               $this->assertEquals( str_replace( ' ', '_', $title ),
+                       $foreignTitle->getFullText() );
+       }
+}
diff --git a/tests/phpunit/unit/includes/title/NamespaceAwareForeignTitleFactoryTest.php b/tests/phpunit/unit/includes/title/NamespaceAwareForeignTitleFactoryTest.php
new file mode 100644 (file)
index 0000000..d777973
--- /dev/null
@@ -0,0 +1,101 @@
+<?php
+/**
+ * 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; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * 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.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @author This, that and the other
+ */
+
+/**
+ * @covers NamespaceAwareForeignTitleFactory
+ *
+ * @group Title
+ */
+class NamespaceAwareForeignTitleFactoryTest extends \MediaWikiUnitTestCase {
+
+       public function basicProvider() {
+               return [
+                       [
+                               'MainNamespaceArticle', 0,
+                               new ForeignTitle( 0, '', 'MainNamespaceArticle' ),
+                       ],
+                       [
+                               'MainNamespaceArticle', null,
+                               new ForeignTitle( 0, '', 'MainNamespaceArticle' ),
+                       ],
+                       [
+                               'Magic:_The_Gathering', 0,
+                               new ForeignTitle( 0, '', 'Magic:_The_Gathering' ),
+                       ],
+                       [
+                               'Talk:Nice_talk', 1,
+                               new ForeignTitle( 1, 'Talk', 'Nice_talk' ),
+                       ],
+                       [
+                               'Talk:Magic:_The_Gathering', 1,
+                               new ForeignTitle( 1, 'Talk', 'Magic:_The_Gathering' ),
+                       ],
+                       [
+                               'Bogus:Nice_talk', 0,
+                               new ForeignTitle( 0, '', 'Bogus:Nice_talk' ),
+                       ],
+                       [
+                               'Bogus:Nice_talk', null,
+                               new ForeignTitle( 9000, 'Bogus', 'Nice_talk' ),
+                       ],
+                       [
+                               'Bogus:Nice_talk', 4,
+                               new ForeignTitle( 4, 'Bogus', 'Nice_talk' ),
+                       ],
+                       [
+                               'Bogus:Nice_talk', 1,
+                               new ForeignTitle( 1, 'Talk', 'Nice_talk' ),
+                       ],
+                       // Misconfigured wiki with unregistered namespace (T114115)
+                       [
+                               'Nice_talk', 1234,
+                               new ForeignTitle( 1234, 'Ns1234', 'Nice_talk' ),
+                       ],
+               ];
+       }
+
+       /**
+        * @dataProvider basicProvider
+        */
+       public function testBasic( $title, $ns, ForeignTitle $foreignTitle ) {
+               $foreignNamespaces = [
+                       0 => '', 1 => 'Talk', 100 => 'Portal', 9000 => 'Bogus'
+               ];
+
+               $factory = new NamespaceAwareForeignTitleFactory( $foreignNamespaces );
+               $testTitle = $factory->createForeignTitle( $title, $ns );
+
+               $this->assertEquals( $testTitle->isNamespaceIdKnown(),
+                       $foreignTitle->isNamespaceIdKnown() );
+
+               if (
+                       $testTitle->isNamespaceIdKnown() &&
+                       $foreignTitle->isNamespaceIdKnown()
+               ) {
+                       $this->assertEquals( $testTitle->getNamespaceId(),
+                               $foreignTitle->getNamespaceId() );
+               }
+
+               $this->assertEquals( $testTitle->getNamespaceName(),
+                       $foreignTitle->getNamespaceName() );
+               $this->assertEquals( $testTitle->getText(), $foreignTitle->getText() );
+       }
+}
diff --git a/tests/phpunit/unit/includes/title/TitleValueTest.php b/tests/phpunit/unit/includes/title/TitleValueTest.php
new file mode 100644 (file)
index 0000000..cd67a93
--- /dev/null
@@ -0,0 +1,149 @@
+<?php
+/**
+ * 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; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * 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.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @author Daniel Kinzler
+ */
+
+/**
+ * @covers TitleValue
+ *
+ * @group Title
+ */
+class TitleValueTest extends \MediaWikiUnitTestCase {
+
+       public function goodConstructorProvider() {
+               return [
+                       [ NS_MAIN, '', 'fragment', '', true, false ],
+                       [ NS_USER, 'TestThis', 'stuff', '', true, false ],
+                       [ NS_USER, 'TestThis', '', 'baz', false, true ],
+               ];
+       }
+
+       /**
+        * @dataProvider goodConstructorProvider
+        */
+       public function testConstruction( $ns, $text, $fragment, $interwiki, $hasFragment,
+               $hasInterwiki
+       ) {
+               $title = new TitleValue( $ns, $text, $fragment, $interwiki );
+
+               $this->assertEquals( $ns, $title->getNamespace() );
+               $this->assertTrue( $title->inNamespace( $ns ) );
+               $this->assertEquals( $text, $title->getText() );
+               $this->assertEquals( $fragment, $title->getFragment() );
+               $this->assertEquals( $hasFragment, $title->hasFragment() );
+               $this->assertEquals( $interwiki, $title->getInterwiki() );
+               $this->assertEquals( $hasInterwiki, $title->isExternal() );
+       }
+
+       public function badConstructorProvider() {
+               return [
+                       [ 'foo', 'title', 'fragment', '' ],
+                       [ null, 'title', 'fragment', '' ],
+                       [ 2.3, 'title', 'fragment', '' ],
+
+                       [ NS_MAIN, 5, 'fragment', '' ],
+                       [ NS_MAIN, null, 'fragment', '' ],
+                       [ NS_USER, '', 'fragment', '' ],
+                       [ NS_MAIN, 'foo bar', '', '' ],
+                       [ NS_MAIN, 'bar_', '', '' ],
+                       [ NS_MAIN, '_foo', '', '' ],
+                       [ NS_MAIN, ' eek ', '', '' ],
+
+                       [ NS_MAIN, 'title', 5, '' ],
+                       [ NS_MAIN, 'title', null, '' ],
+                       [ NS_MAIN, 'title', [], '' ],
+
+                       [ NS_MAIN, 'title', '', 5 ],
+                       [ NS_MAIN, 'title', null, 5 ],
+                       [ NS_MAIN, 'title', [], 5 ],
+               ];
+       }
+
+       /**
+        * @dataProvider badConstructorProvider
+        */
+       public function testConstructionErrors( $ns, $text, $fragment, $interwiki ) {
+               $this->setExpectedException( InvalidArgumentException::class );
+               new TitleValue( $ns, $text, $fragment, $interwiki );
+       }
+
+       public function fragmentTitleProvider() {
+               return [
+                       [ new TitleValue( NS_MAIN, 'Test' ), 'foo' ],
+                       [ new TitleValue( NS_TALK, 'Test', 'foo' ), '' ],
+                       [ new TitleValue( NS_CATEGORY, 'Test', 'foo' ), 'bar' ],
+               ];
+       }
+
+       /**
+        * @dataProvider fragmentTitleProvider
+        */
+       public function testCreateFragmentTitle( TitleValue $title, $fragment ) {
+               $fragmentTitle = $title->createFragmentTarget( $fragment );
+
+               $this->assertEquals( $title->getNamespace(), $fragmentTitle->getNamespace() );
+               $this->assertEquals( $title->getText(), $fragmentTitle->getText() );
+               $this->assertEquals( $fragment, $fragmentTitle->getFragment() );
+       }
+
+       public function getTextProvider() {
+               return [
+                       [ 'Foo', 'Foo' ],
+                       [ 'Foo_Bar', 'Foo Bar' ],
+               ];
+       }
+
+       /**
+        * @dataProvider getTextProvider
+        */
+       public function testGetText( $dbkey, $text ) {
+               $title = new TitleValue( NS_MAIN, $dbkey );
+
+               $this->assertEquals( $text, $title->getText() );
+       }
+
+       public function provideTestToString() {
+               yield [
+                       new TitleValue( 0, 'Foo' ),
+                       '0:Foo'
+               ];
+               yield [
+                       new TitleValue( 1, 'Bar_Baz' ),
+                       '1:Bar_Baz'
+               ];
+               yield [
+                       new TitleValue( 9, 'JoJo', 'Frag' ),
+                       '9:JoJo#Frag'
+               ];
+               yield [
+                       new TitleValue( 200, 'tea', 'Fragment', 'wikicode' ),
+                       'wikicode:200:tea#Fragment'
+               ];
+       }
+
+       /**
+        * @dataProvider provideTestToString
+        */
+       public function testToString( TitleValue $value, $expected ) {
+               $this->assertSame(
+                       $expected,
+                       $value->__toString()
+               );
+       }
+}
diff --git a/tests/phpunit/unit/includes/user/UserArrayFromResultTest.php b/tests/phpunit/unit/includes/user/UserArrayFromResultTest.php
new file mode 100644 (file)
index 0000000..0b2ce17
--- /dev/null
@@ -0,0 +1,110 @@
+<?php
+
+/**
+ * @author Addshore
+ * @covers UserArrayFromResult
+ */
+class UserArrayFromResultTest extends \MediaWikiUnitTestCase {
+
+       private function getMockResultWrapper( $row = null, $numRows = 1 ) {
+               $resultWrapper = $this->getMockBuilder( Wikimedia\Rdbms\ResultWrapper::class )
+                       ->disableOriginalConstructor();
+
+               $resultWrapper = $resultWrapper->getMock();
+               $resultWrapper->expects( $this->atLeastOnce() )
+                       ->method( 'current' )
+                       ->will( $this->returnValue( $row ) );
+               $resultWrapper->expects( $this->any() )
+                       ->method( 'numRows' )
+                       ->will( $this->returnValue( $numRows ) );
+
+               return $resultWrapper;
+       }
+
+       private function getRowWithUsername( $username = 'fooUser' ) {
+               $row = new stdClass();
+               $row->user_name = $username;
+               return $row;
+       }
+
+       /**
+        * @covers UserArrayFromResult::__construct
+        */
+       public function testConstructionWithFalseRow() {
+               $row = false;
+               $resultWrapper = $this->getMockResultWrapper( $row );
+
+               $object = new UserArrayFromResult( $resultWrapper );
+
+               $this->assertEquals( $resultWrapper, $object->res );
+               $this->assertSame( 0, $object->key );
+               $this->assertEquals( $row, $object->current );
+       }
+
+       /**
+        * @covers UserArrayFromResult::__construct
+        */
+       public function testConstructionWithRow() {
+               $username = 'addshore';
+               $row = $this->getRowWithUsername( $username );
+               $resultWrapper = $this->getMockResultWrapper( $row );
+
+               $object = new UserArrayFromResult( $resultWrapper );
+
+               $this->assertEquals( $resultWrapper, $object->res );
+               $this->assertSame( 0, $object->key );
+               $this->assertInstanceOf( User::class, $object->current );
+               $this->assertEquals( $username, $object->current->mName );
+       }
+
+       public static function provideNumberOfRows() {
+               return [
+                       [ 0 ],
+                       [ 1 ],
+                       [ 122 ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideNumberOfRows
+        * @covers UserArrayFromResult::count
+        */
+       public function testCountWithVaryingValues( $numRows ) {
+               $object = new UserArrayFromResult( $this->getMockResultWrapper(
+                       $this->getRowWithUsername(),
+                       $numRows
+               ) );
+               $this->assertEquals( $numRows, $object->count() );
+       }
+
+       /**
+        * @covers UserArrayFromResult::current
+        */
+       public function testCurrentAfterConstruction() {
+               $username = 'addshore';
+               $userRow = $this->getRowWithUsername( $username );
+               $object = new UserArrayFromResult( $this->getMockResultWrapper( $userRow ) );
+               $this->assertInstanceOf( User::class, $object->current() );
+               $this->assertEquals( $username, $object->current()->mName );
+       }
+
+       public function provideTestValid() {
+               return [
+                       [ $this->getRowWithUsername(), true ],
+                       [ false, false ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideTestValid
+        * @covers UserArrayFromResult::valid
+        */
+       public function testValid( $input, $expected ) {
+               $object = new UserArrayFromResult( $this->getMockResultWrapper( $input ) );
+               $this->assertEquals( $expected, $object->valid() );
+       }
+
+       // @todo unit test for key()
+       // @todo unit test for next()
+       // @todo unit test for rewind()
+}
diff --git a/tests/phpunit/unit/includes/utils/AvroValidatorTest.php b/tests/phpunit/unit/includes/utils/AvroValidatorTest.php
new file mode 100644 (file)
index 0000000..cf45f9f
--- /dev/null
@@ -0,0 +1,118 @@
+<?php
+/**
+ * Tests for IP validity functions.
+ *
+ * Ported from /t/inc/IP.t by avar.
+ *
+ * @todo Test methods in this call should be split into a method and a
+ * dataprovider.
+ */
+
+/**
+ * @group IP
+ * @covers AvroValidator
+ */
+class AvroValidatorTest extends PHPUnit\Framework\TestCase {
+
+       use MediaWikiCoversValidator;
+
+       public function setUp() {
+               if ( !class_exists( 'AvroSchema' ) ) {
+                       $this->markTestSkipped( 'Avro is required to run the AvroValidatorTest' );
+               }
+               parent::setUp();
+       }
+
+       public function getErrorsProvider() {
+               $stringSchema = AvroSchema::parse( json_encode( [ 'type' => 'string' ] ) );
+               $stringArraySchema = AvroSchema::parse( json_encode( [
+                       'type' => 'array',
+                       'items' => 'string',
+               ] ) );
+               $recordSchema = AvroSchema::parse( json_encode( [
+                       'type' => 'record',
+                       'name' => 'ut',
+                       'fields' => [
+                               [ 'name' => 'id', 'type' => 'int', 'required' => true ],
+                       ],
+               ] ) );
+               $enumSchema = AvroSchema::parse( json_encode( [
+                       'type' => 'record',
+                       'name' => 'ut',
+                       'fields' => [
+                               [ 'name' => 'count', 'type' => [ 'int', 'null' ] ],
+                       ],
+               ] ) );
+
+               return [
+                       [
+                               'No errors with a simple string serialization',
+                               $stringSchema, 'foobar', [],
+                       ],
+
+                       [
+                               'Cannot serialize integer into string',
+                               $stringSchema, 5, 'Expected string, but recieved integer',
+                       ],
+
+                       [
+                               'Cannot serialize array into string',
+                               $stringSchema, [], 'Expected string, but recieved array',
+                       ],
+
+                       [
+                               'allows and ignores extra fields',
+                               $recordSchema, [ 'id' => 4, 'foo' => 'bar' ], [],
+                       ],
+
+                       [
+                               'detects missing fields',
+                               $recordSchema, [], [ 'id' => 'Missing expected field' ],
+                       ],
+
+                       [
+                               'handles first element in enum',
+                               $enumSchema, [ 'count' => 4 ], [],
+                       ],
+
+                       [
+                               'handles second element in enum',
+                               $enumSchema, [ 'count' => null ], [],
+                       ],
+
+                       [
+                               'rejects element not in union',
+                               $enumSchema, [ 'count' => 'invalid' ], [ 'count' => [
+                                       'Expected any one of these to be true',
+                                       [
+                                               'Expected integer, but recieved string',
+                                               'Expected null, but recieved string',
+                                       ]
+                               ] ]
+                       ],
+                       [
+                               'Empty array is accepted',
+                               $stringArraySchema, [], []
+                       ],
+                       [
+                               'correct array element accepted',
+                               $stringArraySchema, [ 'fizzbuzz' ], []
+                       ],
+                       [
+                               'incorrect array element rejected',
+                               $stringArraySchema, [ '12', 34 ], [ 'Expected string, but recieved integer' ]
+                       ],
+               ];
+       }
+
+       /**
+        * @dataProvider getErrorsProvider
+        */
+       public function testGetErrors( $message, $schema, $datum, $expected ) {
+               $this->assertEquals(
+                       $expected,
+                       AvroValidator::getErrors( $schema, $datum ),
+                       $message
+               );
+       }
+}
diff --git a/tests/phpunit/unit/includes/utils/BatchRowUpdateTest.php b/tests/phpunit/unit/includes/utils/BatchRowUpdateTest.php
new file mode 100644 (file)
index 0000000..92b0d7a
--- /dev/null
@@ -0,0 +1,269 @@
+<?php
+
+use Wikimedia\Rdbms\ILBFactory;
+
+/**
+ * Tests for BatchRowUpdate and its components
+ *
+ * @group db
+ *
+ * @covers BatchRowUpdate
+ * @covers BatchRowIterator
+ * @covers BatchRowWriter
+ */
+class BatchRowUpdateTest extends \MediaWikiUnitTestCase {
+
+       public function testWriterBasicFunctionality() {
+               $lbFactoryMock = $this->createMock( ILBFactory::class );
+               $lbFactoryMockProvider = function () use ( $lbFactoryMock ): ILBFactory {
+                       return $lbFactoryMock;
+               };
+
+               $this->overrideMwServices( [ 'DBLoadBalancerFactory' => $lbFactoryMockProvider ] );
+
+               $db = $this->mockDb( [ 'update' ] );
+               $writer = new BatchRowWriter( $db, 'echo_event' );
+
+               $updates = [
+                       self::mockUpdate( [ 'something' => 'changed' ] ),
+                       self::mockUpdate( [ 'otherthing' => 'changed' ] ),
+                       self::mockUpdate( [ 'and' => 'something', 'else' => 'changed' ] ),
+               ];
+
+               $ticketMock = 'transaction-ticket';
+
+               $db->expects( $this->exactly( count( $updates ) ) )
+                       ->method( 'update' );
+               $lbFactoryMock->expects( $this->any() )
+                       ->method( 'getEmptyTransactionTicket' )
+                       ->willReturn( $ticketMock );
+               $lbFactoryMock->expects( $this->once() )
+                       ->method( 'commitAndWaitForReplication' )
+                       ->with( $this->anything(), $ticketMock );
+
+               $writer->write( $updates );
+       }
+
+       protected static function mockUpdate( array $changes ) {
+               static $i = 0;
+               return [
+                       'primaryKey' => [ 'event_id' => $i++ ],
+                       'changes' => $changes,
+               ];
+       }
+
+       public function testReaderBasicIterate() {
+               $batchSize = 2;
+               $response = $this->genSelectResult( $batchSize, /*numRows*/ 5, function () {
+                       static $i = 0;
+                       return [ 'id_field' => ++$i ];
+               } );
+               $db = $this->mockDbConsecutiveSelect( $response );
+               $reader = new BatchRowIterator( $db, 'some_table', 'id_field', $batchSize );
+
+               $pos = 0;
+               foreach ( $reader as $rows ) {
+                       $this->assertEquals( $response[$pos], $rows, "Testing row in position $pos" );
+                       $pos++;
+               }
+               // -1 is because the final array() marks the end and isnt included
+               $this->assertEquals( count( $response ) - 1, $pos );
+       }
+
+       public static function provider_readerGetPrimaryKey() {
+               $row = [
+                       'id_field' => 42,
+                       'some_col' => 'dvorak',
+                       'other_col' => 'samurai',
+               ];
+               return [
+
+                       [
+                               'Must return single column pk when requested',
+                               [ 'id_field' => 42 ],
+                               $row
+                       ],
+
+                       [
+                               'Must return multiple column pks when requested',
+                               [ 'id_field' => 42, 'other_col' => 'samurai' ],
+                               $row
+                       ],
+
+               ];
+       }
+
+       /**
+        * @dataProvider provider_readerGetPrimaryKey
+        */
+       public function testReaderGetPrimaryKey( $message, array $expected, array $row ) {
+               $reader = new BatchRowIterator( $this->mockDb(), 'some_table', array_keys( $expected ), 8675309 );
+               $this->assertEquals( $expected, $reader->extractPrimaryKeys( (object)$row ), $message );
+       }
+
+       public static function provider_readerSetFetchColumns() {
+               return [
+
+                       [
+                               'Must merge primary keys into select conditions',
+                               // Expected column select
+                               [ 'foo', 'bar' ],
+                               // primary keys
+                               [ 'foo' ],
+                               // setFetchColumn
+                               [ 'bar' ]
+                       ],
+
+                       [
+                               'Must not merge primary keys into the all columns selector',
+                               // Expected column select
+                               [ '*' ],
+                               // primary keys
+                               [ 'foo' ],
+                               // setFetchColumn
+                               [ '*' ],
+                       ],
+
+                       [
+                               'Must not duplicate primary keys into column selector',
+                               // Expected column select.
+                               // TODO: figure out how to only assert the array_values portion and not the keys
+                               [ 0 => 'foo', 1 => 'bar', 3 => 'baz' ],
+                               // primary keys
+                               [ 'foo', 'bar', ],
+                               // setFetchColumn
+                               [ 'bar', 'baz' ],
+                       ],
+               ];
+       }
+
+       /**
+        * @dataProvider provider_readerSetFetchColumns
+        */
+       public function testReaderSetFetchColumns(
+               $message, array $columns, array $primaryKeys, array $fetchColumns
+       ) {
+               $db = $this->mockDb( [ 'select' ] );
+               $db->expects( $this->once() )
+                       ->method( 'select' )
+                       // only testing second parameter of Database::select
+                       ->with( 'some_table', $columns )
+                       ->will( $this->returnValue( new ArrayIterator( [] ) ) );
+
+               $reader = new BatchRowIterator( $db, 'some_table', $primaryKeys, 22 );
+               $reader->setFetchColumns( $fetchColumns );
+               // triggers first database select
+               $reader->rewind();
+       }
+
+       public static function provider_readerSelectConditions() {
+               return [
+
+                       [
+                               "With single primary key must generate id > 'value'",
+                               // Expected second iteration
+                               [ "( id_field > '3' )" ],
+                               // Primary key(s)
+                               'id_field',
+                       ],
+
+                       [
+                               'With multiple primary keys the first conditions ' .
+                                       'must use >= and the final condition must use >',
+                               // Expected second iteration
+                               [ "( id_field = '3' AND foo > '103' ) OR ( id_field > '3' )" ],
+                               // Primary key(s)
+                               [ 'id_field', 'foo' ],
+                       ],
+
+               ];
+       }
+
+       /**
+        * Slightly hackish to use reflection, but asserting different parameters
+        * to consecutive calls of Database::select in phpunit is error prone
+        *
+        * @dataProvider provider_readerSelectConditions
+        */
+       public function testReaderSelectConditionsMultiplePrimaryKeys(
+               $message, $expectedSecondIteration, $primaryKeys, $batchSize = 3
+       ) {
+               $results = $this->genSelectResult( $batchSize, $batchSize * 3, function () {
+                       static $i = 0, $j = 100, $k = 1000;
+                       return [ 'id_field' => ++$i, 'foo' => ++$j, 'bar' => ++$k ];
+               } );
+               $db = $this->mockDbConsecutiveSelect( $results );
+
+               $conditions = [ 'bar' => 42, 'baz' => 'hai' ];
+               $reader = new BatchRowIterator( $db, 'some_table', $primaryKeys, $batchSize );
+               $reader->addConditions( $conditions );
+
+               $buildConditions = new ReflectionMethod( $reader, 'buildConditions' );
+               $buildConditions->setAccessible( true );
+
+               // On first iteration only the passed conditions must be used
+               $this->assertEquals( $conditions, $buildConditions->invoke( $reader ),
+                       'First iteration must return only the conditions passed in addConditions' );
+               $reader->rewind();
+
+               // Second iteration must use the maximum primary key of last set
+               $this->assertEquals(
+                       $conditions + $expectedSecondIteration,
+                       $buildConditions->invoke( $reader ),
+                       $message
+               );
+       }
+
+       protected function mockDbConsecutiveSelect( array $retvals ) {
+               $db = $this->mockDb( [ 'select', 'addQuotes' ] );
+               $db->expects( $this->any() )
+                       ->method( 'select' )
+                       ->will( $this->consecutivelyReturnFromSelect( $retvals ) );
+               $db->expects( $this->any() )
+                       ->method( 'addQuotes' )
+                       ->will( $this->returnCallback( function ( $value ) {
+                               return "'$value'"; // not real quoting: doesn't matter in test
+                       } ) );
+
+               return $db;
+       }
+
+       protected function consecutivelyReturnFromSelect( array $results ) {
+               $retvals = [];
+               foreach ( $results as $rows ) {
+                       // The Database::select method returns iterators, so we do too.
+                       $retvals[] = $this->returnValue( new ArrayIterator( $rows ) );
+               }
+
+               return call_user_func_array( [ $this, 'onConsecutiveCalls' ], $retvals );
+       }
+
+       protected function genSelectResult( $batchSize, $numRows, $rowGenerator ) {
+               $res = [];
+               for ( $i = 0; $i < $numRows; $i += $batchSize ) {
+                       $rows = [];
+                       for ( $j = 0; $j < $batchSize && $i + $j < $numRows; $j++ ) {
+                               $rows [] = (object)call_user_func( $rowGenerator );
+                       }
+                       $res[] = $rows;
+               }
+               $res[] = []; // termination condition requires empty result for last row
+               return $res;
+       }
+
+       protected function mockDb( $methods = [] ) {
+               // @TODO: mock from Database
+               // FIXME: the constructor normally sets mAtomicLevels and mSrvCache
+               $databaseMysql = $this->getMockBuilder( Wikimedia\Rdbms\DatabaseMysqli::class )
+                       ->disableOriginalConstructor()
+                       ->setMethods( array_merge( [ 'isOpen', 'getApproximateLagStatus' ], $methods ) )
+                       ->getMock();
+               $databaseMysql->expects( $this->any() )
+                       ->method( 'isOpen' )
+                       ->will( $this->returnValue( true ) );
+               $databaseMysql->expects( $this->any() )
+                       ->method( 'getApproximateLagStatus' )
+                       ->will( $this->returnValue( [ 'lag' => 0, 'since' => 0 ] ) );
+               return $databaseMysql;
+       }
+}
diff --git a/tests/phpunit/unit/includes/utils/ClassCollectorTest.php b/tests/phpunit/unit/includes/utils/ClassCollectorTest.php
new file mode 100644 (file)
index 0000000..9c7c50f
--- /dev/null
@@ -0,0 +1,60 @@
+<?php
+
+/**
+ * @covers ClassCollector
+ */
+class ClassCollectorTest extends PHPUnit\Framework\TestCase {
+
+       use MediaWikiCoversValidator;
+
+       public static function provideCases() {
+               return [
+                       [
+                               "class Foo {}",
+                               [ 'Foo' ],
+                       ],
+                       [
+                               "namespace Example;\nclass Foo {}\nclass Bar {}",
+                               [ 'Example\Foo', 'Example\Bar' ],
+                       ],
+                       [
+                               "class_alias( 'Foo', 'Bar' );",
+                               [ 'Bar' ],
+                       ],
+                       [
+                               "namespace Example;\nclass Foo {}\nclass_alias( 'Example\Foo', 'Foo' );",
+                               [ 'Example\Foo', 'Foo' ],
+                       ],
+                       [
+                               "namespace Example;\nclass Foo {}\nclass_alias( 'Example\Foo', 'Bar' );",
+                               [ 'Example\Foo', 'Bar' ],
+                       ],
+                       [
+                               "class_alias( Foo::class, 'Bar' );",
+                               [ 'Bar' ],
+                       ],
+                       [
+                               // Namespaced class is not currently supported. Must use namespace declaration
+                               // earlier in the file.
+                               "class_alias( Example\Foo::class, 'Bar' );",
+                               [],
+                       ],
+                       [
+                               "namespace Example;\nclass Foo {}\nclass_alias( Foo::class, 'Bar' );",
+                               [ 'Example\Foo', 'Bar' ],
+                       ],
+                       [
+                               "new class() extends Foo {}",
+                               []
+                       ]
+               ];
+       }
+
+       /**
+        * @dataProvider provideCases
+        */
+       public function testGetClasses( $code, array $classes, $message = null ) {
+               $cc = new ClassCollector();
+               $this->assertEquals( $classes, $cc->getClasses( "<?php\n$code" ), $message );
+       }
+}
diff --git a/tests/phpunit/unit/includes/utils/FileContentsHasherTest.php b/tests/phpunit/unit/includes/utils/FileContentsHasherTest.php
new file mode 100644 (file)
index 0000000..8bf6779
--- /dev/null
@@ -0,0 +1,57 @@
+<?php
+
+/**
+ * @covers FileContentsHasherTest
+ */
+class FileContentsHasherTest extends PHPUnit\Framework\TestCase {
+
+       use MediaWikiCoversValidator;
+
+       public function provideSingleFile() {
+               return array_map( function ( $file ) {
+                       return [ $file, file_get_contents( $file ) ];
+               }, glob( __DIR__ . '/../../../data/filecontentshasher/*.*' ) );
+       }
+
+       public function provideMultipleFiles() {
+               return [
+                       [ $this->provideSingleFile() ]
+               ];
+       }
+
+       /**
+        * @covers FileContentsHasher::getFileContentsHash
+        * @covers FileContentsHasher::getFileContentsHashInternal
+        * @dataProvider provideSingleFile
+        */
+       public function testSingleFileHash( $fileName, $contents ) {
+               foreach ( [ 'md4', 'md5' ] as $algo ) {
+                       $expectedHash = hash( $algo, $contents );
+                       $actualHash = FileContentsHasher::getFileContentsHash( $fileName, $algo );
+                       $this->assertEquals( $expectedHash, $actualHash );
+                       $actualHashRepeat = FileContentsHasher::getFileContentsHash( $fileName, $algo );
+                       $this->assertEquals( $expectedHash, $actualHashRepeat );
+               }
+       }
+
+       /**
+        * @covers FileContentsHasher::getFileContentsHash
+        * @covers FileContentsHasher::getFileContentsHashInternal
+        * @dataProvider provideMultipleFiles
+        */
+       public function testMultipleFileHash( $files ) {
+               $fileNames = [];
+               $hashes = [];
+               foreach ( $files as $fileInfo ) {
+                       list( $fileName, $contents ) = $fileInfo;
+                       $fileNames[] = $fileName;
+                       $hashes[] = md5( $contents );
+               }
+
+               $expectedHash = md5( implode( '', $hashes ) );
+               $actualHash = FileContentsHasher::getFileContentsHash( $fileNames, 'md5' );
+               $this->assertEquals( $expectedHash, $actualHash );
+               $actualHashRepeat = FileContentsHasher::getFileContentsHash( $fileNames, 'md5' );
+               $this->assertEquals( $expectedHash, $actualHashRepeat );
+       }
+}
diff --git a/tests/phpunit/unit/includes/utils/MWCryptHashTest.php b/tests/phpunit/unit/includes/utils/MWCryptHashTest.php
new file mode 100644 (file)
index 0000000..94705bf
--- /dev/null
@@ -0,0 +1,64 @@
+<?php
+
+/**
+ * @group Hash
+ *
+ * @covers MWCryptHash
+ */
+class MWCryptHashTest extends PHPUnit\Framework\TestCase {
+
+       use MediaWikiCoversValidator;
+
+       public function testHashLength() {
+               if ( MWCryptHash::hashAlgo() !== 'whirlpool' ) {
+                       $this->markTestSkipped( 'Hash algorithm isn\'t whirlpool' );
+               }
+
+               $this->assertEquals( 64, MWCryptHash::hashLength(), 'Raw hash length' );
+               $this->assertEquals( 128, MWCryptHash::hashLength( false ), 'Hex hash length' );
+       }
+
+       public function testHash() {
+               if ( MWCryptHash::hashAlgo() !== 'whirlpool' ) {
+                       $this->markTestSkipped( 'Hash algorithm isn\'t whirlpool' );
+               }
+
+               $data = 'foobar';
+               // phpcs:ignore Generic.Files.LineLength
+               $hash = '9923afaec3a86f865bb231a588f453f84e8151a2deb4109aebc6de4284be5bebcff4fab82a7e51d920237340a043736e9d13bab196006dcca0fe65314d68eab9';
+
+               $this->assertEquals(
+                       hex2bin( $hash ),
+                       MWCryptHash::hash( $data ),
+                       'Raw hash'
+               );
+               $this->assertEquals(
+                       $hash,
+                       MWCryptHash::hash( $data, false ),
+                       'Hex hash'
+               );
+       }
+
+       public function testHmac() {
+               if ( MWCryptHash::hashAlgo() !== 'whirlpool' ) {
+                       $this->markTestSkipped( 'Hash algorithm isn\'t whirlpool' );
+               }
+
+               $data = 'foobar';
+               $key = 'secret';
+               // phpcs:ignore Generic.Files.LineLength
+               $hash = 'ddc94177b2020e55ce2049199fd9cc6327f416ff6dc621cc34cb43d9bec61d73372b4790c0e24957f565ecaf2d42821e6303619093e99cbe14a3b9250bda5f81';
+
+               $this->assertEquals(
+                       hex2bin( $hash ),
+                       MWCryptHash::hmac( $data, $key ),
+                       'Raw hmac'
+               );
+               $this->assertEquals(
+                       $hash,
+                       MWCryptHash::hmac( $data, $key, false ),
+                       'Hex hmac'
+               );
+       }
+
+}
diff --git a/tests/phpunit/unit/includes/utils/MWRestrictionsTest.php b/tests/phpunit/unit/includes/utils/MWRestrictionsTest.php
new file mode 100644 (file)
index 0000000..abdfbb1
--- /dev/null
@@ -0,0 +1,217 @@
+<?php
+class MWRestrictionsTest extends PHPUnit\Framework\TestCase {
+
+       use MediaWikiCoversValidator;
+
+       protected static $restrictionsForChecks;
+
+       public static function setUpBeforeClass() {
+               self::$restrictionsForChecks = MWRestrictions::newFromArray( [
+                       'IPAddresses' => [
+                               '10.0.0.0/8',
+                               '172.16.0.0/12',
+                               '2001:db8::/33',
+                       ]
+               ] );
+       }
+
+       /**
+        * @covers MWRestrictions::newDefault
+        * @covers MWRestrictions::__construct
+        */
+       public function testNewDefault() {
+               $ret = MWRestrictions::newDefault();
+               $this->assertInstanceOf( MWRestrictions::class, $ret );
+               $this->assertSame(
+                       '{"IPAddresses":["0.0.0.0/0","::/0"]}',
+                       $ret->toJson()
+               );
+       }
+
+       /**
+        * @covers MWRestrictions::newFromArray
+        * @covers MWRestrictions::__construct
+        * @covers MWRestrictions::loadFromArray
+        * @covers MWRestrictions::toArray
+        * @dataProvider provideArray
+        * @param array $data
+        * @param bool|InvalidArgumentException $expect True if the call succeeds,
+        *  otherwise the exception that should be thrown.
+        */
+       public function testArray( $data, $expect ) {
+               if ( $expect === true ) {
+                       $ret = MWRestrictions::newFromArray( $data );
+                       $this->assertInstanceOf( MWRestrictions::class, $ret );
+                       $this->assertSame( $data, $ret->toArray() );
+               } else {
+                       try {
+                               MWRestrictions::newFromArray( $data );
+                               $this->fail( 'Expected exception not thrown' );
+                       } catch ( InvalidArgumentException $ex ) {
+                               $this->assertEquals( $expect, $ex );
+                       }
+               }
+       }
+
+       public static function provideArray() {
+               return [
+                       [ [ 'IPAddresses' => [] ], true ],
+                       [ [ 'IPAddresses' => [ '127.0.0.1/32' ] ], true ],
+                       [
+                               [ 'IPAddresses' => [ '256.0.0.1/32' ] ],
+                               new InvalidArgumentException( 'Invalid IP address: 256.0.0.1/32' )
+                       ],
+                       [
+                               [ 'IPAddresses' => '127.0.0.1/32' ],
+                               new InvalidArgumentException( 'IPAddresses is not an array' )
+                       ],
+                       [
+                               [],
+                               new InvalidArgumentException( 'Array is missing required keys: IPAddresses' )
+                       ],
+                       [
+                               [ 'foo' => 'bar', 'bar' => 42 ],
+                               new InvalidArgumentException( 'Array contains invalid keys: foo, bar' )
+                       ],
+               ];
+       }
+
+       /**
+        * @covers MWRestrictions::newFromJson
+        * @covers MWRestrictions::__construct
+        * @covers MWRestrictions::loadFromArray
+        * @covers MWRestrictions::toJson
+        * @covers MWRestrictions::__toString
+        * @dataProvider provideJson
+        * @param string $json
+        * @param array|InvalidArgumentException $expect
+        */
+       public function testJson( $json, $expect ) {
+               if ( is_array( $expect ) ) {
+                       $ret = MWRestrictions::newFromJson( $json );
+                       $this->assertInstanceOf( MWRestrictions::class, $ret );
+                       $this->assertSame( $expect, $ret->toArray() );
+
+                       $this->assertSame( $json, $ret->toJson( false ) );
+                       $this->assertSame( $json, (string)$ret );
+
+                       $this->assertSame(
+                               FormatJson::encode( $expect, true, FormatJson::ALL_OK ),
+                               $ret->toJson( true )
+                       );
+               } else {
+                       try {
+                               MWRestrictions::newFromJson( $json );
+                               $this->fail( 'Expected exception not thrown' );
+                       } catch ( InvalidArgumentException $ex ) {
+                               $this->assertTrue( true );
+                       }
+               }
+       }
+
+       public static function provideJson() {
+               return [
+                       [
+                               '{"IPAddresses":[]}',
+                               [ 'IPAddresses' => [] ]
+                       ],
+                       [
+                               '{"IPAddresses":["127.0.0.1/32"]}',
+                               [ 'IPAddresses' => [ '127.0.0.1/32' ] ]
+                       ],
+                       [
+                               '{"IPAddresses":["256.0.0.1/32"]}',
+                               new InvalidArgumentException( 'Invalid IP address: 256.0.0.1/32' )
+                       ],
+                       [
+                               '{"IPAddresses":"127.0.0.1/32"}',
+                               new InvalidArgumentException( 'IPAddresses is not an array' )
+                       ],
+                       [
+                               '{}',
+                               new InvalidArgumentException( 'Array is missing required keys: IPAddresses' )
+                       ],
+                       [
+                               '{"foo":"bar","bar":42}',
+                               new InvalidArgumentException( 'Array contains invalid keys: foo, bar' )
+                       ],
+                       [
+                               '{"IPAddresses":[]',
+                               new InvalidArgumentException( 'Invalid restrictions JSON' )
+                       ],
+                       [
+                               '"IPAddresses"',
+                               new InvalidArgumentException( 'Invalid restrictions JSON' )
+                       ],
+               ];
+       }
+
+       /**
+        * @covers MWRestrictions::checkIP
+        * @dataProvider provideCheckIP
+        * @param string $ip
+        * @param bool $pass
+        */
+       public function testCheckIP( $ip, $pass ) {
+               $this->assertSame( $pass, self::$restrictionsForChecks->checkIP( $ip ) );
+       }
+
+       public static function provideCheckIP() {
+               return [
+                       [ '10.0.0.1', true ],
+                       [ '172.16.0.0', true ],
+                       [ '192.0.2.1', false ],
+                       [ '2001:db8:1::', true ],
+                       [ '2001:0db8:0000:0000:0000:0000:0000:0000', true ],
+                       [ '2001:0DB8:8000::', false ],
+               ];
+       }
+
+       /**
+        * @covers MWRestrictions::check
+        * @dataProvider provideCheck
+        * @param WebRequest $request
+        * @param Status $expect
+        */
+       public function testCheck( $request, $expect ) {
+               $this->assertEquals( $expect, self::$restrictionsForChecks->check( $request ) );
+       }
+
+       public function provideCheck() {
+               $ret = [];
+
+               $mockBuilder = $this->getMockBuilder( FauxRequest::class )
+                       ->setMethods( [ 'getIP' ] );
+
+               foreach ( self::provideCheckIP() as $checkIP ) {
+                       $ok = [];
+                       $request = $mockBuilder->getMock();
+
+                       $request->expects( $this->any() )->method( 'getIP' )
+                               ->will( $this->returnValue( $checkIP[0] ) );
+                       $ok['ip'] = $checkIP[1];
+
+                       /* If we ever add more restrictions, add nested for loops here:
+                        *  foreach ( self::provideCheckFoo() as $checkFoo ) {
+                        *      $request->expects( $this->any() )->method( 'getFoo' )
+                        *          ->will( $this->returnValue( $checkFoo[0] );
+                        *      $ok['foo'] = $checkFoo[1];
+                        *
+                        *      foreach ( self::provideCheckBar() as $checkBar ) {
+                        *          $request->expects( $this->any() )->method( 'getBar' )
+                        *              ->will( $this->returnValue( $checkBar[0] );
+                        *          $ok['bar'] = $checkBar[1];
+                        *
+                        *          // etc.
+                        *      }
+                        *  }
+                        */
+
+                       $status = Status::newGood();
+                       $status->setResult( $ok === array_filter( $ok ), $ok );
+                       $ret[] = [ $request, $status ];
+               }
+
+               return $ret;
+       }
+}
diff --git a/tests/phpunit/unit/includes/utils/UIDGeneratorTest.php b/tests/phpunit/unit/includes/utils/UIDGeneratorTest.php
new file mode 100644 (file)
index 0000000..6b81a66
--- /dev/null
@@ -0,0 +1,177 @@
+<?php
+
+class UIDGeneratorTest extends PHPUnit\Framework\TestCase {
+
+       use MediaWikiCoversValidator;
+
+       protected function tearDown() {
+               // T46850
+               UIDGenerator::unitTestTearDown();
+               parent::tearDown();
+       }
+
+       /**
+        * Test that generated UIDs have the expected properties
+        *
+        * @dataProvider provider_testTimestampedUID
+        * @covers UIDGenerator::newTimestampedUID88
+        * @covers UIDGenerator::getTimestampedID88
+        * @covers UIDGenerator::newTimestampedUID128
+        * @covers UIDGenerator::getTimestampedID128
+        */
+       public function testTimestampedUID( $method, $digitlen, $bits, $tbits, $hostbits ) {
+               $id = call_user_func( [ UIDGenerator::class, $method ] );
+               $this->assertEquals( true, ctype_digit( $id ), "UID made of digit characters" );
+               $this->assertLessThanOrEqual( $digitlen, strlen( $id ),
+                       "UID has the right number of digits" );
+               $this->assertLessThanOrEqual( $bits, strlen( Wikimedia\base_convert( $id, 10, 2 ) ),
+                       "UID has the right number of bits" );
+
+               $ids = [];
+               for ( $i = 0; $i < 300; $i++ ) {
+                       $ids[] = call_user_func( [ UIDGenerator::class, $method ] );
+               }
+
+               $lastId = array_shift( $ids );
+
+               $this->assertSame( array_unique( $ids ), $ids, "All generated IDs are unique." );
+
+               foreach ( $ids as $id ) {
+                       // Convert string to binary and pad to full length so we can
+                       // extract segments
+                       $id_bin = Wikimedia\base_convert( $id, 10, 2, $bits );
+                       $lastId_bin = Wikimedia\base_convert( $lastId, 10, 2, $bits );
+
+                       $timestamp_bin = substr( $id_bin, 0, $tbits );
+                       $last_timestamp_bin = substr( $lastId_bin, 0, $tbits );
+
+                       $this->assertGreaterThanOrEqual(
+                               $last_timestamp_bin,
+                               $timestamp_bin,
+                               "timestamp ($timestamp_bin) of current ID ($id_bin) >= timestamp ($last_timestamp_bin) " .
+                                       "of prior one ($lastId_bin)" );
+
+                       $hostbits_bin = substr( $id_bin, -$hostbits );
+                       $last_hostbits_bin = substr( $lastId_bin, -$hostbits );
+
+                       if ( $hostbits ) {
+                               $this->assertEquals(
+                                       $hostbits_bin,
+                                       $last_hostbits_bin,
+                                       "Host ID ($hostbits_bin) of current ID ($id_bin) is same as host ID ($last_hostbits_bin) " .
+                                               "of prior one ($lastId_bin)." );
+                       }
+
+                       $lastId = $id;
+               }
+       }
+
+       /**
+        * array( method, length, bits, hostbits )
+        * NOTE: When adding a new method name here please update the covers tags for the tests!
+        */
+       public static function provider_testTimestampedUID() {
+               return [
+                       [ 'newTimestampedUID128', 39, 128, 46, 48 ],
+                       [ 'newTimestampedUID128', 39, 128, 46, 48 ],
+                       [ 'newTimestampedUID88', 27, 88, 46, 32 ],
+               ];
+       }
+
+       /**
+        * @covers UIDGenerator::newUUIDv1
+        * @covers UIDGenerator::getUUIDv1
+        */
+       public function testUUIDv1() {
+               $ids = [];
+               for ( $i = 0; $i < 100; $i++ ) {
+                       $id = UIDGenerator::newUUIDv1();
+                       $this->assertEquals( true,
+                               preg_match( '!^[0-9a-f]{8}-[0-9a-f]{4}-1[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$!', $id ),
+                               "UID $id has the right format" );
+                       $ids[] = $id;
+
+                       $id = UIDGenerator::newRawUUIDv1();
+                       $this->assertEquals( true,
+                               preg_match( '!^[0-9a-f]{12}1[0-9a-f]{3}[89ab][0-9a-f]{15}$!', $id ),
+                               "UID $id has the right format" );
+
+                       $id = UIDGenerator::newRawUUIDv1();
+                       $this->assertEquals( true,
+                               preg_match( '!^[0-9a-f]{12}1[0-9a-f]{3}[89ab][0-9a-f]{15}$!', $id ),
+                               "UID $id has the right format" );
+               }
+
+               $this->assertEquals( array_unique( $ids ), $ids, "All generated IDs are unique." );
+       }
+
+       /**
+        * @covers UIDGenerator::newUUIDv4
+        */
+       public function testUUIDv4() {
+               $ids = [];
+               for ( $i = 0; $i < 100; $i++ ) {
+                       $id = UIDGenerator::newUUIDv4();
+                       $ids[] = $id;
+                       $this->assertEquals( true,
+                               preg_match( '!^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$!', $id ),
+                               "UID $id has the right format" );
+               }
+
+               $this->assertEquals( array_unique( $ids ), $ids, 'All generated IDs are unique.' );
+       }
+
+       /**
+        * @covers UIDGenerator::newRawUUIDv4
+        */
+       public function testRawUUIDv4() {
+               for ( $i = 0; $i < 100; $i++ ) {
+                       $id = UIDGenerator::newRawUUIDv4();
+                       $this->assertEquals( true,
+                               preg_match( '!^[0-9a-f]{12}4[0-9a-f]{3}[89ab][0-9a-f]{15}$!', $id ),
+                               "UID $id has the right format" );
+               }
+       }
+
+       /**
+        * @covers UIDGenerator::newRawUUIDv4
+        */
+       public function testRawUUIDv4QuickRand() {
+               for ( $i = 0; $i < 100; $i++ ) {
+                       $id = UIDGenerator::newRawUUIDv4( UIDGenerator::QUICK_RAND );
+                       $this->assertEquals( true,
+                               preg_match( '!^[0-9a-f]{12}4[0-9a-f]{3}[89ab][0-9a-f]{15}$!', $id ),
+                               "UID $id has the right format" );
+               }
+       }
+
+       /**
+        * @covers UIDGenerator::newSequentialPerNodeID
+        */
+       public function testNewSequentialID() {
+               $id1 = UIDGenerator::newSequentialPerNodeID( 'test', 32 );
+               $id2 = UIDGenerator::newSequentialPerNodeID( 'test', 32 );
+
+               $this->assertInternalType( 'float', $id1, "ID returned as float" );
+               $this->assertInternalType( 'float', $id2, "ID returned as float" );
+               $this->assertGreaterThan( 0, $id1, "ID greater than 1" );
+               $this->assertGreaterThan( $id1, $id2, "IDs increasing in value" );
+       }
+
+       /**
+        * @covers UIDGenerator::newSequentialPerNodeIDs
+        * @covers UIDGenerator::getSequentialPerNodeIDs
+        */
+       public function testNewSequentialIDs() {
+               $ids = UIDGenerator::newSequentialPerNodeIDs( 'test', 32, 5 );
+               $lastId = null;
+               foreach ( $ids as $id ) {
+                       $this->assertInternalType( 'float', $id, "ID returned as float" );
+                       $this->assertGreaterThan( 0, $id, "ID greater than 1" );
+                       if ( $lastId ) {
+                               $this->assertGreaterThan( $lastId, $id, "IDs increasing in value" );
+                       }
+                       $lastId = $id;
+               }
+       }
+}
diff --git a/tests/phpunit/unit/includes/utils/ZipDirectoryReaderTest.php b/tests/phpunit/unit/includes/utils/ZipDirectoryReaderTest.php
new file mode 100644 (file)
index 0000000..e8252a1
--- /dev/null
@@ -0,0 +1,88 @@
+<?php
+
+/**
+ * @covers ZipDirectoryReader
+ * NOTE: this test is more like an integration test than a unit test
+ */
+class ZipDirectoryReaderTest extends PHPUnit\Framework\TestCase {
+
+       use MediaWikiCoversValidator;
+
+       protected $zipDir;
+       protected $entries;
+
+       protected function setUp() {
+               parent::setUp();
+               $this->zipDir = __DIR__ . '/../../../data/zip';
+       }
+
+       function zipCallback( $entry ) {
+               $this->entries[] = $entry;
+       }
+
+       function readZipAssertError( $file, $error, $assertMessage ) {
+               $this->entries = [];
+               $status = ZipDirectoryReader::read( "{$this->zipDir}/$file", [ $this, 'zipCallback' ] );
+               $this->assertTrue( $status->hasMessage( $error ), $assertMessage );
+       }
+
+       function readZipAssertSuccess( $file, $assertMessage ) {
+               $this->entries = [];
+               $status = ZipDirectoryReader::read( "{$this->zipDir}/$file", [ $this, 'zipCallback' ] );
+               $this->assertTrue( $status->isOK(), $assertMessage );
+       }
+
+       public function testEmpty() {
+               $this->readZipAssertSuccess( 'empty.zip', 'Empty zip' );
+       }
+
+       public function testMultiDisk0() {
+               $this->readZipAssertError( 'split.zip', 'zip-unsupported',
+                       'Split zip error' );
+       }
+
+       public function testNoSignature() {
+               $this->readZipAssertError( 'nosig.zip', 'zip-wrong-format',
+                       'No signature should give "wrong format" error' );
+       }
+
+       public function testSimple() {
+               $this->readZipAssertSuccess( 'class.zip', 'Simple ZIP' );
+               $this->assertEquals( $this->entries, [ [
+                       'name' => 'Class.class',
+                       'mtime' => '20010115000000',
+                       'size' => 1,
+               ] ] );
+       }
+
+       public function testBadCentralEntrySignature() {
+               $this->readZipAssertError( 'wrong-central-entry-sig.zip', 'zip-bad',
+                       'Bad central entry error' );
+       }
+
+       public function testTrailingBytes() {
+               // Due to T40432 this is now zip-wrong-format instead of zip-bad
+               $this->readZipAssertError( 'trail.zip', 'zip-wrong-format',
+                       'Trailing bytes error' );
+       }
+
+       public function testWrongCDStart() {
+               $this->readZipAssertError( 'wrong-cd-start-disk.zip', 'zip-unsupported',
+                       'Wrong CD start disk error' );
+       }
+
+       public function testCentralDirectoryGap() {
+               $this->readZipAssertError( 'cd-gap.zip', 'zip-bad',
+                       'CD gap error' );
+       }
+
+       public function testCentralDirectoryTruncated() {
+               $this->readZipAssertError( 'cd-truncated.zip', 'zip-bad',
+                       'CD truncated error (should hit unpack() overrun)' );
+       }
+
+       public function testLooksLikeZip64() {
+               $this->readZipAssertError( 'looks-like-zip64.zip', 'zip-unsupported',
+                       'A file which looks like ZIP64 but isn\'t, should give error' );
+       }
+}
diff --git a/tests/phpunit/unit/includes/watcheditem/NoWriteWatchedItemStoreUnitTest.php b/tests/phpunit/unit/includes/watcheditem/NoWriteWatchedItemStoreUnitTest.php
new file mode 100644 (file)
index 0000000..556f518
--- /dev/null
@@ -0,0 +1,250 @@
+<?php
+
+use MediaWiki\User\UserIdentityValue;
+
+/**
+ * @author Addshore
+ *
+ * @covers NoWriteWatchedItemStore
+ */
+class NoWriteWatchedItemStoreUnitTest extends \MediaWikiUnitTestCase {
+
+       public function testAddWatch() {
+               /** @var WatchedItemStoreInterface|PHPUnit_Framework_MockObject_MockObject $innerService */
+               $innerService = $this->getMockForAbstractClass( WatchedItemStoreInterface::class );
+               $innerService->expects( $this->never() )->method( 'addWatch' );
+               $noWriteService = new NoWriteWatchedItemStore( $innerService );
+
+               $this->setExpectedException( DBReadOnlyError::class );
+               $noWriteService->addWatch(
+                       new UserIdentityValue( 1, 'MockUser', 0 ), new TitleValue( 0, 'Foo' ) );
+       }
+
+       public function testAddWatchBatchForUser() {
+               /** @var WatchedItemStoreInterface|PHPUnit_Framework_MockObject_MockObject $innerService */
+               $innerService = $this->getMockForAbstractClass( WatchedItemStoreInterface::class );
+               $innerService->expects( $this->never() )->method( 'addWatchBatchForUser' );
+               $noWriteService = new NoWriteWatchedItemStore( $innerService );
+
+               $this->setExpectedException( DBReadOnlyError::class );
+               $noWriteService->addWatchBatchForUser( new UserIdentityValue( 1, 'MockUser', 0 ), [] );
+       }
+
+       public function testRemoveWatch() {
+               /** @var WatchedItemStoreInterface|PHPUnit_Framework_MockObject_MockObject $innerService */
+               $innerService = $this->getMockForAbstractClass( WatchedItemStoreInterface::class );
+               $innerService->expects( $this->never() )->method( 'removeWatch' );
+               $noWriteService = new NoWriteWatchedItemStore( $innerService );
+
+               $this->setExpectedException( DBReadOnlyError::class );
+               $noWriteService->removeWatch(
+                       new UserIdentityValue( 1, 'MockUser', 0 ), new TitleValue( 0, 'Foo' ) );
+       }
+
+       public function testSetNotificationTimestampsForUser() {
+               /** @var WatchedItemStoreInterface|PHPUnit_Framework_MockObject_MockObject $innerService */
+               $innerService = $this->getMockForAbstractClass( WatchedItemStoreInterface::class );
+               $innerService->expects( $this->never() )->method( 'setNotificationTimestampsForUser' );
+               $noWriteService = new NoWriteWatchedItemStore( $innerService );
+
+               $this->setExpectedException( DBReadOnlyError::class );
+               $noWriteService->setNotificationTimestampsForUser(
+                       new UserIdentityValue( 1, 'MockUser', 0 ),
+                       'timestamp',
+                       []
+               );
+       }
+
+       public function testUpdateNotificationTimestamp() {
+               /** @var WatchedItemStoreInterface|PHPUnit_Framework_MockObject_MockObject $innerService */
+               $innerService = $this->getMockForAbstractClass( WatchedItemStoreInterface::class );
+               $innerService->expects( $this->never() )->method( 'updateNotificationTimestamp' );
+               $noWriteService = new NoWriteWatchedItemStore( $innerService );
+
+               $this->setExpectedException( DBReadOnlyError::class );
+               $noWriteService->updateNotificationTimestamp(
+                       new UserIdentityValue( 1, 'MockUser', 0 ),
+                       new TitleValue( 0, 'Foo' ),
+                       'timestamp'
+               );
+       }
+
+       public function testResetNotificationTimestamp() {
+               /** @var WatchedItemStoreInterface|PHPUnit_Framework_MockObject_MockObject $innerService */
+               $innerService = $this->getMockForAbstractClass( WatchedItemStoreInterface::class );
+               $innerService->expects( $this->never() )->method( 'resetNotificationTimestamp' );
+               $noWriteService = new NoWriteWatchedItemStore( $innerService );
+
+               $this->setExpectedException( DBReadOnlyError::class );
+               $noWriteService->resetNotificationTimestamp(
+                       new UserIdentityValue( 1, 'MockUser', 0 ),
+                       new TitleValue( 0, 'Foo' )
+               );
+       }
+
+       public function testCountWatchedItems() {
+               /** @var WatchedItemStoreInterface|PHPUnit_Framework_MockObject_MockObject $innerService */
+               $innerService = $this->getMockForAbstractClass( WatchedItemStoreInterface::class );
+               $innerService->expects( $this->once() )->method( 'countWatchedItems' )->willReturn( __METHOD__ );
+               $noWriteService = new NoWriteWatchedItemStore( $innerService );
+
+               $return = $noWriteService->countWatchedItems(
+                       new UserIdentityValue( 1, 'MockUser', 0 )
+               );
+               $this->assertEquals( __METHOD__, $return );
+       }
+
+       public function testCountWatchers() {
+               /** @var WatchedItemStoreInterface|PHPUnit_Framework_MockObject_MockObject $innerService */
+               $innerService = $this->getMockForAbstractClass( WatchedItemStoreInterface::class );
+               $innerService->expects( $this->once() )->method( 'countWatchers' )->willReturn( __METHOD__ );
+               $noWriteService = new NoWriteWatchedItemStore( $innerService );
+
+               $return = $noWriteService->countWatchers(
+                       new TitleValue( 0, 'Foo' )
+               );
+               $this->assertEquals( __METHOD__, $return );
+       }
+
+       public function testCountVisitingWatchers() {
+               /** @var WatchedItemStoreInterface|PHPUnit_Framework_MockObject_MockObject $innerService */
+               $innerService = $this->getMockForAbstractClass( WatchedItemStoreInterface::class );
+               $innerService->expects( $this->once() )
+                       ->method( 'countVisitingWatchers' )
+                       ->willReturn( __METHOD__ );
+               $noWriteService = new NoWriteWatchedItemStore( $innerService );
+
+               $return = $noWriteService->countVisitingWatchers(
+                       new TitleValue( 0, 'Foo' ),
+                       9
+               );
+               $this->assertEquals( __METHOD__, $return );
+       }
+
+       public function testCountWatchersMultiple() {
+               /** @var WatchedItemStoreInterface|PHPUnit_Framework_MockObject_MockObject $innerService */
+               $innerService = $this->getMockForAbstractClass( WatchedItemStoreInterface::class );
+               $innerService->expects( $this->once() )
+                       ->method( 'countVisitingWatchersMultiple' )
+                       ->willReturn( __METHOD__ );
+               $noWriteService = new NoWriteWatchedItemStore( $innerService );
+
+               $return = $noWriteService->countWatchersMultiple(
+                       [ new TitleValue( 0, 'Foo' ) ],
+                       []
+               );
+               $this->assertEquals( __METHOD__, $return );
+       }
+
+       public function testCountVisitingWatchersMultiple() {
+               /** @var WatchedItemStoreInterface|PHPUnit_Framework_MockObject_MockObject $innerService */
+               $innerService = $this->getMockForAbstractClass( WatchedItemStoreInterface::class );
+               $innerService->expects( $this->once() )
+                       ->method( 'countVisitingWatchersMultiple' )
+                       ->willReturn( __METHOD__ );
+               $noWriteService = new NoWriteWatchedItemStore( $innerService );
+
+               $return = $noWriteService->countVisitingWatchersMultiple(
+                       [ [ new TitleValue( 0, 'Foo' ), 99 ] ],
+                       11
+               );
+               $this->assertEquals( __METHOD__, $return );
+       }
+
+       public function testGetWatchedItem() {
+               /** @var WatchedItemStoreInterface|PHPUnit_Framework_MockObject_MockObject $innerService */
+               $innerService = $this->getMockForAbstractClass( WatchedItemStoreInterface::class );
+               $innerService->expects( $this->once() )->method( 'getWatchedItem' )->willReturn( __METHOD__ );
+               $noWriteService = new NoWriteWatchedItemStore( $innerService );
+
+               $return = $noWriteService->getWatchedItem(
+                       new UserIdentityValue( 1, 'MockUser', 0 ),
+                       new TitleValue( 0, 'Foo' )
+               );
+               $this->assertEquals( __METHOD__, $return );
+       }
+
+       public function testLoadWatchedItem() {
+               /** @var WatchedItemStoreInterface|PHPUnit_Framework_MockObject_MockObject $innerService */
+               $innerService = $this->getMockForAbstractClass( WatchedItemStoreInterface::class );
+               $innerService->expects( $this->once() )->method( 'loadWatchedItem' )->willReturn( __METHOD__ );
+               $noWriteService = new NoWriteWatchedItemStore( $innerService );
+
+               $return = $noWriteService->loadWatchedItem(
+                       new UserIdentityValue( 1, 'MockUser', 0 ),
+                       new TitleValue( 0, 'Foo' )
+               );
+               $this->assertEquals( __METHOD__, $return );
+       }
+
+       public function testGetWatchedItemsForUser() {
+               /** @var WatchedItemStoreInterface|PHPUnit_Framework_MockObject_MockObject $innerService */
+               $innerService = $this->getMockForAbstractClass( WatchedItemStoreInterface::class );
+               $innerService->expects( $this->once() )
+                       ->method( 'getWatchedItemsForUser' )
+                       ->willReturn( __METHOD__ );
+               $noWriteService = new NoWriteWatchedItemStore( $innerService );
+
+               $return = $noWriteService->getWatchedItemsForUser(
+                       new UserIdentityValue( 1, 'MockUser', 0 ),
+                       []
+               );
+               $this->assertEquals( __METHOD__, $return );
+       }
+
+       public function testIsWatched() {
+               /** @var WatchedItemStoreInterface|PHPUnit_Framework_MockObject_MockObject $innerService */
+               $innerService = $this->getMockForAbstractClass( WatchedItemStoreInterface::class );
+               $innerService->expects( $this->once() )->method( 'isWatched' )->willReturn( __METHOD__ );
+               $noWriteService = new NoWriteWatchedItemStore( $innerService );
+
+               $return = $noWriteService->isWatched(
+                       new UserIdentityValue( 1, 'MockUser', 0 ),
+                       new TitleValue( 0, 'Foo' )
+               );
+               $this->assertEquals( __METHOD__, $return );
+       }
+
+       public function testGetNotificationTimestampsBatch() {
+               /** @var WatchedItemStoreInterface|PHPUnit_Framework_MockObject_MockObject $innerService */
+               $innerService = $this->getMockForAbstractClass( WatchedItemStoreInterface::class );
+               $innerService->expects( $this->once() )
+                       ->method( 'getNotificationTimestampsBatch' )
+                       ->willReturn( __METHOD__ );
+               $noWriteService = new NoWriteWatchedItemStore( $innerService );
+
+               $return = $noWriteService->getNotificationTimestampsBatch(
+                       new UserIdentityValue( 1, 'MockUser', 0 ),
+                       [ new TitleValue( 0, 'Foo' ) ]
+               );
+               $this->assertEquals( __METHOD__, $return );
+       }
+
+       public function testCountUnreadNotifications() {
+               /** @var WatchedItemStoreInterface|PHPUnit_Framework_MockObject_MockObject $innerService */
+               $innerService = $this->getMockForAbstractClass( WatchedItemStoreInterface::class );
+               $innerService->expects( $this->once() )
+                       ->method( 'countUnreadNotifications' )
+                       ->willReturn( __METHOD__ );
+               $noWriteService = new NoWriteWatchedItemStore( $innerService );
+
+               $return = $noWriteService->countUnreadNotifications(
+                       new UserIdentityValue( 1, 'MockUser', 0 ),
+                       88
+               );
+               $this->assertEquals( __METHOD__, $return );
+       }
+
+       public function testDuplicateAllAssociatedEntries() {
+               /** @var WatchedItemStoreInterface|PHPUnit_Framework_MockObject_MockObject $innerService */
+               $innerService = $this->getMockForAbstractClass( WatchedItemStoreInterface::class );
+               $noWriteService = new NoWriteWatchedItemStore( $innerService );
+
+               $this->setExpectedException( DBReadOnlyError::class );
+               $noWriteService->duplicateAllAssociatedEntries(
+                       new TitleValue( 0, 'Foo' ),
+                       new TitleValue( 0, 'Bar' )
+               );
+       }
+
+}
diff --git a/tests/phpunit/unit/initUnitTests.php b/tests/phpunit/unit/initUnitTests.php
new file mode 100644 (file)
index 0000000..ef32cab
--- /dev/null
@@ -0,0 +1,56 @@
+<?php
+
+/**
+ * Allows to include a file that assumes to be included in the file scope.
+ * It makes all globals available in the inclusion scope before including the file,
+ * then exports all new globals.
+ *
+ * @param string $fileName the file to include
+ */
+function wfRequireOnceInGlobalScope( $fileName ) {
+       // phpcs:disable MediaWiki.Usage.ForbiddenFunctions.extract
+       extract( $GLOBALS, EXTR_REFS | EXTR_SKIP );
+       // phpcs:enable
+
+       require_once $fileName;
+
+       foreach ( get_defined_vars() as $varName => $value ) {
+               $GLOBALS[$varName] = $value;
+       }
+}
+
+define( 'MEDIAWIKI', true );
+define( 'MW_PHPUNIT_TEST', true );
+
+// Inject test configuration via callback, bypassing LocalSettings.php
+define( 'MW_CONFIG_CALLBACK', '\TestSetup::applyInitialConfig' );
+// We don't use a settings file here but some code still assumes that one exists
+define( 'MW_CONFIG_FILE', 'LocalSettings.php' );
+
+$IP = realpath( __DIR__ . '/../../..' );
+
+// these variables must be defined before setup runs
+$GLOBALS['IP'] = $IP;
+$GLOBALS['wgCommandLineMode'] = true;
+
+// Bypass Setup.php's scope test
+$GLOBALS['wgScopeTest'] = 'MediaWiki Setup.php scope test';
+// Avoid PHP Notice in Setup.php
+$GLOBALS['self'] = 'Unit tests';
+
+require_once "$IP/tests/common/TestSetup.php";
+
+wfRequireOnceInGlobalScope( "$IP/includes/AutoLoader.php" );
+wfRequireOnceInGlobalScope( "$IP/includes/Defines.php" );
+wfRequireOnceInGlobalScope( "$IP/includes/DefaultSettings.php" );
+wfRequireOnceInGlobalScope( "$IP/includes/Setup.php" );
+
+require_once "$IP/tests/common/TestsAutoLoader.php";
+
+// Remove MWExceptionHandler's handling of PHP errors to allow PHPUnit to replace them
+restore_error_handler();
+
+unset( $GLOBALS['wgScopeTest'] );
+
+// Disable all database connections
+\MediaWiki\MediaWikiServices::disableStorageBackend();
diff --git a/tests/phpunit/unit/languages/SpecialPageAliasTest.php b/tests/phpunit/unit/languages/SpecialPageAliasTest.php
new file mode 100644 (file)
index 0000000..cce9d0e
--- /dev/null
@@ -0,0 +1,64 @@
+<?php
+
+/**
+ * Verifies that special page aliases are valid, with no slashes.
+ *
+ * @group Language
+ * @group SpecialPageAliases
+ * @group SystemTest
+ * @group medium
+ * @todo This should be a structure test
+ *
+ * @author Katie Filbert < aude.wiki@gmail.com >
+ */
+class SpecialPageAliasTest extends \MediaWikiUnitTestCase {
+
+       /**
+        * @coversNothing
+        * @dataProvider validSpecialPageAliasesProvider
+        */
+       public function testValidSpecialPageAliases( $code, $specialPageAliases ) {
+               foreach ( $specialPageAliases as $specialPage => $aliases ) {
+                       foreach ( $aliases as $alias ) {
+                               $msg = "$specialPage alias '$alias' in $code is valid with no slashes";
+                               $this->assertRegExp( '/^[^\/]*$/', $msg );
+                       }
+               }
+       }
+
+       public function validSpecialPageAliasesProvider() {
+               $codes = array_keys( Language::fetchLanguageNames( null, 'mwfile' ) );
+
+               $data = [];
+
+               foreach ( $codes as $code ) {
+                       $specialPageAliases = $this->getSpecialPageAliases( $code );
+
+                       if ( $specialPageAliases !== [] ) {
+                               $data[] = [ $code, $specialPageAliases ];
+                       }
+               }
+
+               return $data;
+       }
+
+       /**
+        * @param string $code
+        *
+        * @return array
+        */
+       protected function getSpecialPageAliases( $code ) {
+               $file = Language::getMessagesFileName( $code );
+
+               if ( is_readable( $file ) ) {
+                       include $file;
+
+                       if ( isset( $specialPageAliases ) && $specialPageAliases !== null ) {
+                               return $specialPageAliases;
+                       }
+               }
+
+               return [];
+       }
+
+}
diff --git a/tests/phpunit/unit/structure/ApiPrefixUniquenessTest.php b/tests/phpunit/unit/structure/ApiPrefixUniquenessTest.php
new file mode 100644 (file)
index 0000000..b937fab
--- /dev/null
@@ -0,0 +1,49 @@
+<?php
+
+/**
+ * Checks that all API query modules, core and extensions, have unique prefixes.
+ *
+ * @group API
+ * @coversNothing
+ */
+class ApiPrefixUniquenessTest extends \MediaWikiUnitTestCase {
+
+       public function testPrefixes() {
+               $main = new ApiMain( new FauxRequest() );
+               $query = new ApiQuery( $main, 'foo' );
+               $moduleManager = $query->getModuleManager();
+
+               $modules = $moduleManager->getNames();
+               $prefixes = [];
+
+               foreach ( $modules as $name ) {
+                       $module = $moduleManager->getModule( $name );
+                       $class = get_class( $module );
+
+                       $prefix = $module->getModulePrefix();
+                       if ( $prefix === '' /* HACK: T196962 */ || $prefix === 'wbeu' ) {
+                               continue;
+                       }
+
+                       if ( isset( $prefixes[$prefix] ) ) {
+                               $this->fail(
+                                       "Module prefix '{$prefix}' is shared between {$class} and {$prefixes[$prefix]}"
+                               );
+                       }
+                       $prefixes[$module->getModulePrefix()] = $class;
+
+                       if ( $module instanceof ApiQueryGeneratorBase ) {
+                               // namespace with 'g', a generator can share a prefix with a module
+                               $prefix = 'g' . $prefix;
+                               if ( isset( $prefixes[$prefix] ) ) {
+                                       $this->fail(
+                                               "Module prefix '{$prefix}' is shared between {$class} and {$prefixes[$prefix]}" .
+                                                       " (as a generator)"
+                                       );
+                               }
+                               $prefixes[$module->getModulePrefix()] = $class;
+                       }
+               }
+               $this->assertTrue( true ); // dummy call to make this test non-incomplete
+       }
+}
diff --git a/tests/phpunit/unit/structure/AutoLoaderStructureTest.php b/tests/phpunit/unit/structure/AutoLoaderStructureTest.php
new file mode 100644 (file)
index 0000000..e91159d
--- /dev/null
@@ -0,0 +1,215 @@
+<?php
+
+/**
+ * @coversNothing
+ */
+class AutoLoaderStructureTest extends MediaWikiUnitTestCase {
+
+       /**
+        * Assert that there were no classes loaded that are not registered with the AutoLoader.
+        *
+        * For example foo.php having class Foo and class Bar but only registering Foo.
+        * This is important because we should not be relying on Foo being used before Bar.
+        */
+       public function testAutoLoadConfig() {
+               $results = self::checkAutoLoadConf();
+
+               $this->assertEquals(
+                       $results['expected'],
+                       $results['actual']
+               );
+       }
+
+       public function providePSR4Completeness() {
+               foreach ( AutoLoader::$psr4Namespaces as $prefix => $dir ) {
+                       foreach ( $this->recurseFiles( $dir ) as $file ) {
+                               yield [ $prefix, $dir, $file ];
+                       }
+               }
+       }
+
+       private function recurseFiles( $dir ) {
+               return ( new File_Iterator_Facade() )->getFilesAsArray( $dir, [ '.php' ] );
+       }
+
+       /**
+        * @dataProvider providePSR4Completeness
+        */
+       public function testPSR4Completeness( $prefix, $dir, $file ) {
+               global $wgAutoloadLocalClasses, $wgAutoloadClasses;
+               $contents = file_get_contents( $file );
+               list( $classesInFile, $aliasesInFile ) = self::parseFile( $contents );
+               $classes = array_keys( $classesInFile );
+               if ( $classes ) {
+                       $this->assertCount(
+                               1,
+                               $classes,
+                               "Only one class per file in PSR-4 autoloaded classes ($file)"
+                       );
+
+                       // Check that the expected class name (based on the filename) is the
+                       // same as the one we found.
+                       // Strip directory prefix from front of filename, and .php extension
+                       $dirNameLength = strlen( realpath( $dir ) ) + 1; // +1 for the trailing slash
+                       $fileBaseName = substr( $file, $dirNameLength );
+                       $abbrFileName = substr( $fileBaseName, 0, -4 );
+                       $expectedClassName = $prefix . str_replace( '/', '\\', $abbrFileName );
+
+                       $this->assertSame(
+                               $expectedClassName,
+                               $classes[0],
+                               "Class not autoloaded properly"
+                       );
+
+               } else {
+                       // Dummy assertion so this test isn't marked in risky
+                       // if the file has no classes nor aliases in it
+                       $this->assertCount( 0, $classes );
+               }
+
+               if ( $aliasesInFile ) {
+                       $otherClasses = $wgAutoloadLocalClasses + $wgAutoloadClasses;
+                       foreach ( $aliasesInFile as $alias => $class ) {
+                               $this->assertArrayHasKey( $alias, $otherClasses,
+                                       'Alias must be in the classmap autoloader'
+                               );
+                       }
+               }
+       }
+
+       private static function parseFile( $contents ) {
+               // We could use token_get_all() here, but this is faster
+               // Note: Keep in sync with ClassCollector
+               $matches = [];
+               preg_match_all( '/
+                               ^ [\t ]* (?:
+                                       (?:final\s+)? (?:abstract\s+)? (?:class|interface|trait) \s+
+                                       (?P<class> \w+)
+                               |
+                                       class_alias \s* \( \s*
+                                               ([\'"]) (?P<original> [^\'"]+) \g{-2} \s* , \s*
+                                               ([\'"]) (?P<alias> [^\'"]+ ) \g{-2} \s*
+                                       \) \s* ;
+                               |
+                                       class_alias \s* \( \s*
+                                               (?P<originalStatic> [\w\\\\]+)::class \s* , \s*
+                                               ([\'"]) (?P<aliasString> [^\'"]+ ) \g{-2} \s*
+                                       \) \s* ;
+                               )
+                       /imx', $contents, $matches, PREG_SET_ORDER );
+
+               $namespaceMatch = [];
+               preg_match( '/
+                               ^ [\t ]*
+                                       namespace \s+
+                                               (\w+(\\\\\w+)*)
+                                       \s* ;
+                       /imx', $contents, $namespaceMatch );
+               $fileNamespace = $namespaceMatch ? $namespaceMatch[1] . '\\' : '';
+
+               $classesInFile = [];
+               $aliasesInFile = [];
+
+               foreach ( $matches as $match ) {
+                       if ( !empty( $match['class'] ) ) {
+                               // 'class Foo {}'
+                               $class = $fileNamespace . $match['class'];
+                               $classesInFile[$class] = true;
+                       } elseif ( !empty( $match['original'] ) ) {
+                               // 'class_alias( "Foo", "Bar" );'
+                               $aliasesInFile[$match['alias']] = $match['original'];
+                       } else {
+                               // 'class_alias( Foo::class, "Bar" );'
+                               $aliasesInFile[$match['aliasString']] = $fileNamespace . $match['originalStatic'];
+                       }
+               }
+
+               return [ $classesInFile, $aliasesInFile ];
+       }
+
+       protected static function checkAutoLoadConf() {
+               global $wgAutoloadLocalClasses, $wgAutoloadClasses, $IP;
+
+               // wgAutoloadLocalClasses has precedence, just like in includes/AutoLoader.php
+               $expected = $wgAutoloadLocalClasses + $wgAutoloadClasses;
+               $actual = [];
+
+               $psr4Namespaces = [];
+               foreach ( AutoLoader::getAutoloadNamespaces() as $ns => $path ) {
+                       $psr4Namespaces[rtrim( $ns, '\\' ) . '\\'] = rtrim( $path, '/' );
+               }
+
+               foreach ( $expected as $class => $file ) {
+                       // Only prefix $IP if it doesn't have it already.
+                       // Generally local classes don't have it, and those from extensions and test suites do.
+                       if ( substr( $file, 0, 1 ) != '/' && substr( $file, 1, 1 ) != ':' ) {
+                               $filePath = "$IP/$file";
+                       } else {
+                               $filePath = $file;
+                       }
+
+                       if ( !file_exists( $filePath ) ) {
+                               $actual[$class] = "[file '$filePath' does not exist]";
+                               continue;
+                       }
+
+                       Wikimedia\suppressWarnings();
+                       $contents = file_get_contents( $filePath );
+                       Wikimedia\restoreWarnings();
+
+                       if ( $contents === false ) {
+                               $actual[$class] = "[couldn't read file '$filePath']";
+                               continue;
+                       }
+
+                       list( $classesInFile, $aliasesInFile ) = self::parseFile( $contents );
+
+                       foreach ( $classesInFile as $className => $ignore ) {
+                               // Skip if it's a PSR4 class
+                               $parts = explode( '\\', $className );
+                               for ( $i = count( $parts ) - 1; $i > 0; $i-- ) {
+                                       $ns = implode( '\\', array_slice( $parts, 0, $i ) ) . '\\';
+                                       if ( isset( $psr4Namespaces[$ns] ) ) {
+                                               $expectedPath = $psr4Namespaces[$ns] . '/'
+                                                       . implode( '/', array_slice( $parts, $i ) )
+                                                       . '.php';
+                                               if ( $filePath === $expectedPath ) {
+                                                       continue 2;
+                                               }
+                                       }
+                               }
+
+                               // Nope, add it.
+                               $actual[$className] = $file;
+                       }
+
+                       // Only accept aliases for classes in the same file, because for correct
+                       // behavior, all aliases for a class must be set up when the class is loaded
+                       // (see <https://bugs.php.net/bug.php?id=61422>).
+                       foreach ( $aliasesInFile as $alias => $class ) {
+                               if ( isset( $classesInFile[$class] ) ) {
+                                       $actual[$alias] = $file;
+                               } else {
+                                       $actual[$alias] = "[original class not in $file]";
+                               }
+                       }
+               }
+
+               return [
+                       'expected' => $expected,
+                       'actual' => $actual,
+               ];
+       }
+
+       public function testAutoloadOrder() {
+               $path = realpath( __DIR__ . '/../../../..' );
+               $oldAutoload = file_get_contents( $path . '/autoload.php' );
+               $generator = new AutoloadGenerator( $path, 'local' );
+               $generator->setPsr4Namespaces( AutoLoader::getAutoloadNamespaces() );
+               $generator->initMediaWikiDefault();
+               $newAutoload = $generator->getAutoload( 'maintenance/generateLocalAutoload.php' );
+
+               $this->assertEquals( $oldAutoload, $newAutoload, 'autoload.php does not match' .
+                       ' output of generateLocalAutoload.php script.' );
+       }
+}
diff --git a/tests/phpunit/unit/structure/ContentHandlerSanityTest.php b/tests/phpunit/unit/structure/ContentHandlerSanityTest.php
new file mode 100644 (file)
index 0000000..7541e59
--- /dev/null
@@ -0,0 +1,62 @@
+<?php
+
+use Wikimedia\TestingAccessWrapper;
+
+/**
+ * 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; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * 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.
+ * http://www.gnu.org/copyleft/gpl.html
+ */
+
+/**
+ * @coversNothing
+ */
+class ContentHandlerSanityTest extends \MediaWikiUnitTestCase {
+
+       public static function provideHandlers() {
+               $models = ContentHandler::getContentModels();
+               $handlers = [];
+               foreach ( $models as $model ) {
+                       $handlers[] = [ ContentHandler::getForModelID( $model ) ];
+               }
+
+               return $handlers;
+       }
+
+       /**
+        * @dataProvider provideHandlers
+        * @param ContentHandler $handler
+        */
+       public function testMakeEmptyContent( ContentHandler $handler ) {
+               $content = $handler->makeEmptyContent();
+               $this->assertInstanceOf( Content::class, $content );
+               if ( $handler instanceof TextContentHandler ) {
+                       // TextContentHandler::getContentClass() is protected, so bypass
+                       // that restriction
+                       $testingWrapper = TestingAccessWrapper::newFromObject( $handler );
+                       $this->assertInstanceOf( $testingWrapper->getContentClass(), $content );
+               }
+
+               $handlerClass = get_class( $handler );
+               $contentClass = get_class( $content );
+
+               if ( $handler->supportsDirectEditing() ) {
+                       $this->assertTrue(
+                               $content->isValid(),
+                               "$handlerClass::makeEmptyContent() did not return a valid content ($contentClass::isValid())"
+                       );
+               }
+       }
+
+}
diff --git a/tests/phpunit/unit/structure/PasswordPolicyStructureTest.php b/tests/phpunit/unit/structure/PasswordPolicyStructureTest.php
new file mode 100644 (file)
index 0000000..7867722
--- /dev/null
@@ -0,0 +1,49 @@
+<?php
+
+/**
+ * @coversNothing
+ */
+class PasswordPolicyStructureTest extends \MediaWikiUnitTestCase {
+
+       public function provideChecks() {
+               global $wgPasswordPolicy;
+
+               foreach ( $wgPasswordPolicy['checks'] as $name => $callback ) {
+                       yield [ $name ];
+               }
+       }
+
+       public function provideFlags() {
+               global $wgPasswordPolicy;
+
+               // This won't actually find all flags, just the ones in use. Can't really be helped,
+               // other than adding the core flags here.
+               $flags = [ 'forceChange', 'suggestChangeOnLogin' ];
+               foreach ( $wgPasswordPolicy['policies'] as $group => $checks ) {
+                       foreach ( $checks as $check => $settings ) {
+                               if ( is_array( $settings ) ) {
+                                       $flags = array_unique(
+                                               array_merge( $flags, array_diff( array_keys( $settings ), [ 'value' ] ) )
+                                       );
+                               }
+                       }
+               }
+
+               foreach ( $flags as $flag ) {
+                       yield [ $flag ];
+               }
+       }
+
+       /** @dataProvider provideChecks */
+       public function testCheckMessage( $check ) {
+               $msg = wfMessage( 'passwordpolicies-policy-' . strtolower( $check ) );
+               $this->assertTrue( $msg->exists() );
+       }
+
+       /** @dataProvider provideFlags */
+       public function testFlagMessage( $flag ) {
+               $msg = wfMessage( 'passwordpolicies-policyflag-' . strtolower( $flag ) );
+               $this->assertTrue( $msg->exists() );
+       }
+
+}